49
43
from ivle import util
52
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
49
from ivle.webapp.admin.traversal import (root_to_subject,
53
50
subject_to_offering, offering_to_projectset, offering_to_project,
54
offering_to_enrolment, subject_url, semester_url, offering_url,
55
projectset_url, project_url, enrolment_url)
56
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
57
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
58
ProjectsBreadcrumb, EnrolmentBreadcrumb)
59
from ivle.webapp.core import Plugin as CorePlugin
60
from ivle.webapp.groups import GroupsView
61
from ivle.webapp.media import media_url
62
from ivle.webapp.tutorial import Plugin as TutorialPlugin
51
subject_url, offering_url, projectset_url, project_url)
64
53
class SubjectsView(XHTMLView):
65
54
'''The view of the list of subjects.'''
66
55
template = 'templates/subjects.html'
68
breadcrumb_text = "Subjects"
70
58
def authorize(self, req):
71
59
return req.user is not None
73
61
def populate(self, req, ctx):
75
62
ctx['user'] = req.user
76
63
ctx['semesters'] = []
78
for semester in req.store.find(Semester).order_by(
79
Desc(Semester.year), Desc(Semester.display_name)):
81
# For admins, show all subjects in the system
82
offerings = list(semester.offerings.find())
84
offerings = [enrolment.offering for enrolment in
85
semester.enrolments.find(user=req.user)]
87
ctx['semesters'].append((semester, offerings))
90
class SubjectsManage(XHTMLView):
91
'''Subject management view.'''
92
template = 'templates/subjects-manage.html'
95
def authorize(self, req):
96
return req.user is not None and req.user.admin
98
def populate(self, req, ctx):
100
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
101
ctx['SubjectView'] = SubjectView
102
ctx['SubjectEdit'] = SubjectEdit
103
ctx['SemesterEdit'] = SemesterEdit
105
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
106
ctx['semesters'] = req.store.find(Semester).order_by(
107
Semester.year, Semester.display_name)
110
class SubjectUniquenessValidator(formencode.FancyValidator):
111
"""A FormEncode validator that checks that a subject attribute is unique.
113
The subject referenced by state.existing_subject is permitted
114
to hold that name. If any other object holds it, the input is rejected.
116
:param attribute: the name of the attribute to check.
117
:param display: a string to identify the field in case of error.
120
def __init__(self, attribute, display):
121
self.attribute = attribute
122
self.display = display
124
def _to_python(self, value, state):
125
if (state.store.find(Subject, **{self.attribute: value}).one() not in
126
(None, state.existing_subject)):
127
raise formencode.Invalid(
128
'%s already taken' % self.display, value, state)
132
class SubjectSchema(formencode.Schema):
133
short_name = formencode.All(
134
SubjectUniquenessValidator('short_name', 'URL name'),
135
URLNameValidator(not_empty=True))
136
name = formencode.validators.UnicodeString(not_empty=True)
137
code = formencode.All(
138
SubjectUniquenessValidator('code', 'Subject code'),
139
formencode.validators.UnicodeString(not_empty=True))
142
class SubjectFormView(BaseFormView):
143
"""An abstract form to add or edit a subject."""
146
def authorize(self, req):
147
return req.user is not None and req.user.admin
149
def populate_state(self, state):
150
state.existing_subject = None
154
return SubjectSchema()
157
class SubjectNew(SubjectFormView):
158
"""A form to create a subject."""
159
template = 'templates/subject-new.html'
161
def get_default_data(self, req):
164
def save_object(self, req, data):
165
new_subject = Subject()
166
new_subject.short_name = data['short_name']
167
new_subject.name = data['name']
168
new_subject.code = data['code']
170
req.store.add(new_subject)
174
class SubjectEdit(SubjectFormView):
175
"""A form to edit a subject."""
176
template = 'templates/subject-edit.html'
178
def populate_state(self, state):
179
state.existing_subject = self.context
181
def get_default_data(self, req):
183
'short_name': self.context.short_name,
184
'name': self.context.name,
185
'code': self.context.code,
188
def save_object(self, req, data):
189
self.context.short_name = data['short_name']
190
self.context.name = data['name']
191
self.context.code = data['code']
196
class SemesterUniquenessValidator(formencode.FancyValidator):
197
"""A FormEncode validator that checks that a semester is unique.
199
There cannot be more than one semester for the same year and semester.
201
def _to_python(self, value, state):
202
if (state.store.find(
203
Semester, year=value['year'], url_name=value['url_name']
204
).one() not in (None, state.existing_semester)):
205
raise formencode.Invalid(
206
'Semester already exists', value, state)
210
class SemesterSchema(formencode.Schema):
211
year = URLNameValidator()
212
code = formencode.validators.UnicodeString()
213
url_name = URLNameValidator()
214
display_name = formencode.validators.UnicodeString()
215
state = formencode.All(
216
formencode.validators.OneOf(["past", "current", "future"]),
217
formencode.validators.UnicodeString())
218
chained_validators = [SemesterUniquenessValidator()]
221
class SemesterFormView(BaseFormView):
224
def authorize(self, req):
225
return req.user is not None and req.user.admin
229
return SemesterSchema()
231
def get_return_url(self, obj):
232
return '/subjects/+manage'
235
class SemesterNew(SemesterFormView):
236
"""A form to create a semester."""
237
template = 'templates/semester-new.html'
240
def populate_state(self, state):
241
state.existing_semester = None
243
def get_default_data(self, req):
246
def save_object(self, req, data):
247
new_semester = Semester()
248
new_semester.year = data['year']
249
new_semester.code = data['code']
250
new_semester.url_name = data['url_name']
251
new_semester.display_name = data['display_name']
252
new_semester.state = data['state']
254
req.store.add(new_semester)
258
class SemesterEdit(SemesterFormView):
259
"""A form to edit a semester."""
260
template = 'templates/semester-edit.html'
262
def populate_state(self, state):
263
state.existing_semester = self.context
265
def get_default_data(self, req):
267
'year': self.context.year,
268
'code': self.context.code,
269
'url_name': self.context.url_name,
270
'display_name': self.context.display_name,
271
'state': self.context.state,
274
def save_object(self, req, data):
275
self.context.year = data['year']
276
self.context.code = data['code']
277
self.context.url_name = data['url_name']
278
self.context.display_name = data['display_name']
279
self.context.state = data['state']
283
class SubjectView(XHTMLView):
284
'''The view of the list of offerings in a given subject.'''
285
template = 'templates/subject.html'
288
def authorize(self, req):
289
return req.user is not None
291
def populate(self, req, ctx):
292
ctx['context'] = self.context
294
ctx['user'] = req.user
295
ctx['offerings'] = list(self.context.offerings)
296
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
297
ctx['SubjectEdit'] = SubjectEdit
298
ctx['SubjectOfferingNew'] = SubjectOfferingNew
301
class OfferingView(XHTMLView):
302
"""The home page of an offering."""
303
template = 'templates/offering.html'
307
def populate(self, req, ctx):
308
# Need the worksheet result styles.
309
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
310
ctx['context'] = self.context
312
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
313
ctx['format_submission_principal'] = util.format_submission_principal
314
ctx['format_datetime'] = ivle.date.make_date_nice
315
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
316
ctx['OfferingEdit'] = OfferingEdit
317
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
318
ctx['GroupsView'] = GroupsView
319
ctx['EnrolmentsView'] = EnrolmentsView
320
ctx['Project'] = ivle.database.Project
322
# As we go, calculate the total score for this subject
323
# (Assessable worksheets only, mandatory problems only)
325
ctx['worksheets'], problems_total, problems_done = (
326
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
327
req.config, req.store, req.user, self.context,
328
as_of=self.context.worksheet_cutoff))
330
ctx['exercises_total'] = problems_total
331
ctx['exercises_done'] = problems_done
332
if problems_total > 0:
333
if problems_done >= problems_total:
334
ctx['worksheets_complete_class'] = "complete"
335
elif problems_done > 0:
336
ctx['worksheets_complete_class'] = "semicomplete"
338
ctx['worksheets_complete_class'] = "incomplete"
339
# Calculate the final percentage and mark for the subject
340
(ctx['exercises_pct'], ctx['worksheet_mark'],
341
ctx['worksheet_max_mark']) = (
342
ivle.worksheet.utils.calculate_mark(
343
problems_done, problems_total))
346
class SubjectValidator(formencode.FancyValidator):
347
"""A FormEncode validator that turns a subject name into a subject.
349
The state must have a 'store' attribute, which is the Storm store
352
def _to_python(self, value, state):
353
subject = state.store.find(Subject, short_name=value).one()
357
raise formencode.Invalid('Subject does not exist', value, state)
360
class SemesterValidator(formencode.FancyValidator):
361
"""A FormEncode validator that turns a string into a semester.
363
The string should be of the form 'year/semester', eg. '2009/1'.
365
The state must have a 'store' attribute, which is the Storm store
368
def _to_python(self, value, state):
370
year, semester = value.split('/')
372
year = semester = None
374
semester = state.store.find(
375
Semester, year=year, url_name=semester).one()
379
raise formencode.Invalid('Semester does not exist', value, state)
382
class OfferingUniquenessValidator(formencode.FancyValidator):
383
"""A FormEncode validator that checks that an offering is unique.
385
There cannot be more than one offering in the same year and semester.
387
The offering referenced by state.existing_offering is permitted to
388
hold that year and semester tuple. If any other object holds it, the
391
def _to_python(self, value, state):
392
if (state.store.find(
393
Offering, subject=value['subject'],
394
semester=value['semester']).one() not in
395
(None, state.existing_offering)):
396
raise formencode.Invalid(
397
'Offering already exists', value, state)
401
class OfferingSchema(formencode.Schema):
402
description = formencode.validators.UnicodeString(
403
if_missing=None, not_empty=False)
404
url = formencode.validators.URL(if_missing=None, not_empty=False)
405
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
406
show_worksheet_marks = formencode.validators.StringBoolean(
410
class OfferingAdminSchema(OfferingSchema):
411
subject = formencode.All(
412
SubjectValidator(), formencode.validators.UnicodeString())
413
semester = formencode.All(
414
SemesterValidator(), formencode.validators.UnicodeString())
415
chained_validators = [OfferingUniquenessValidator()]
418
class OfferingEdit(BaseFormView):
419
"""A form to edit an offering's details."""
420
template = 'templates/offering-edit.html'
426
if self.req.user.admin:
427
return OfferingAdminSchema()
429
return OfferingSchema()
431
def populate(self, req, ctx):
432
super(OfferingEdit, self).populate(req, ctx)
433
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
434
ctx['semesters'] = req.store.find(Semester).order_by(
435
Semester.year, Semester.display_name)
436
ctx['force_subject'] = None
438
def populate_state(self, state):
439
state.existing_offering = self.context
441
def get_default_data(self, req):
443
'subject': self.context.subject.short_name,
444
'semester': self.context.semester.year + '/' +
445
self.context.semester.url_name,
446
'url': self.context.url,
447
'description': self.context.description,
448
'worksheet_cutoff': self.context.worksheet_cutoff,
449
'show_worksheet_marks': self.context.show_worksheet_marks,
452
def save_object(self, req, data):
454
self.context.subject = data['subject']
455
self.context.semester = data['semester']
456
self.context.description = data['description']
457
self.context.url = unicode(data['url']) if data['url'] else None
458
self.context.worksheet_cutoff = data['worksheet_cutoff']
459
self.context.show_worksheet_marks = data['show_worksheet_marks']
463
class OfferingNew(BaseFormView):
464
"""A form to create an offering."""
465
template = 'templates/offering-new.html'
468
def authorize(self, req):
469
return req.user is not None and req.user.admin
473
return OfferingAdminSchema()
475
def populate(self, req, ctx):
476
super(OfferingNew, self).populate(req, ctx)
477
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
478
ctx['semesters'] = req.store.find(Semester).order_by(
479
Semester.year, Semester.display_name)
480
ctx['force_subject'] = None
482
def populate_state(self, state):
483
state.existing_offering = None
485
def get_default_data(self, req):
488
def save_object(self, req, data):
489
new_offering = Offering()
490
new_offering.subject = data['subject']
491
new_offering.semester = data['semester']
492
new_offering.description = data['description']
493
new_offering.url = unicode(data['url']) if data['url'] else None
494
new_offering.worksheet_cutoff = data['worksheet_cutoff']
495
new_offering.show_worksheet_marks = data['show_worksheet_marks']
497
req.store.add(new_offering)
500
class SubjectOfferingNew(OfferingNew):
501
"""A form to create an offering for a given subject."""
502
# Identical to OfferingNew, except it forces the subject to be the subject
504
def populate(self, req, ctx):
505
super(SubjectOfferingNew, self).populate(req, ctx)
506
ctx['force_subject'] = self.context
508
class OfferingCloneWorksheetsSchema(formencode.Schema):
509
subject = formencode.All(
510
SubjectValidator(), formencode.validators.UnicodeString())
511
semester = formencode.All(
512
SemesterValidator(), formencode.validators.UnicodeString())
515
class OfferingCloneWorksheets(BaseFormView):
516
"""A form to clone worksheets from one offering to another."""
517
template = 'templates/offering-clone-worksheets.html'
520
def authorize(self, req):
521
return req.user is not None and req.user.admin
525
return OfferingCloneWorksheetsSchema()
527
def populate(self, req, ctx):
528
super(OfferingCloneWorksheets, self).populate(req, ctx)
529
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
530
ctx['semesters'] = req.store.find(Semester).order_by(
531
Semester.year, Semester.display_name)
533
def get_default_data(self, req):
536
def save_object(self, req, data):
537
if self.context.worksheets.count() > 0:
539
"Cannot clone to target with existing worksheets.")
540
offering = req.store.find(
541
Offering, subject=data['subject'], semester=data['semester']).one()
543
raise BadRequest("No such offering.")
544
if offering.worksheets.count() == 0:
545
raise BadRequest("Source offering has no worksheets.")
547
self.context.clone_worksheets(offering)
64
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
65
Desc(Semester.semester)):
66
enrolments = semester.enrolments.find(user=req.user)
67
if enrolments.count():
68
ctx['semesters'].append((semester, enrolments))
551
71
class UserValidator(formencode.FancyValidator):
640
124
ctx['data'] = data or {}
641
125
ctx['offering'] = self.context
642
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
643
126
ctx['errors'] = errors
644
# If all of the fields validated, set the global form error.
645
if isinstance(errors, basestring):
646
ctx['error_value'] = errors
649
class EnrolmentEditSchema(formencode.Schema):
650
role = formencode.All(formencode.validators.OneOf(
651
["lecturer", "tutor", "student"]),
652
RoleEnrolmentValidator(),
653
formencode.validators.UnicodeString())
656
class EnrolmentEdit(BaseFormView):
657
"""A form to alter an enrolment's role."""
658
template = 'templates/enrolment-edit.html'
662
def populate_state(self, state):
663
state.offering = self.context.offering
665
def get_default_data(self, req):
666
return {'role': self.context.role}
670
return EnrolmentEditSchema()
672
def save_object(self, req, data):
673
self.context.role = data['role']
675
def get_return_url(self, obj):
676
return self.req.publisher.generate(
677
self.context.offering, EnrolmentsView)
679
def populate(self, req, ctx):
680
super(EnrolmentEdit, self).populate(req, ctx)
681
ctx['offering_perms'] = self.context.offering.get_permissions(
682
req.user, req.config)
685
class EnrolmentDelete(XHTMLView):
686
"""A form to alter an enrolment's role."""
687
template = 'templates/enrolment-delete.html'
691
def populate(self, req, ctx):
692
# If POSTing, delete delete delete.
693
if req.method == 'POST':
694
self.context.delete()
696
req.throw_redirect(req.publisher.generate(
697
self.context.offering, EnrolmentsView))
699
ctx['enrolment'] = self.context
702
128
class OfferingProjectsView(XHTMLView):
703
129
"""View the projects for an offering."""
704
130
template = 'templates/offering_projects.html'
705
131
permission = 'edit'
707
breadcrumb_text = 'Projects'
134
def project_url(self, projectset, project):
135
return "/subjects/%s/%s/%s/+projects/%s" % (
136
self.context.subject.short_name,
137
self.context.semester.year,
138
self.context.semester.semester,
142
def new_project_url(self, projectset):
143
return "/api/subjects/" + self.context.subject.short_name + "/" +\
144
self.context.semester.year + "/" + \
145
self.context.semester.semester + "/+projectsets/" +\
146
str(projectset.id) + "/+projects/+new"
709
148
def populate(self, req, ctx):
710
149
self.plugin_styles[Plugin] = ["project.css"]
150
self.plugin_scripts[Plugin] = ["project.js"]
712
151
ctx['offering'] = self.context
713
152
ctx['projectsets'] = []
715
154
#Open the projectset Fragment, and render it for inclusion
716
155
#into the ProjectSets page
156
#XXX: This could be a lot cleaner
157
loader = genshi.template.TemplateLoader(".", auto_reload=True)
717
159
set_fragment = os.path.join(os.path.dirname(__file__),
718
160
"templates/projectset_fragment.html")
719
161
project_fragment = os.path.join(os.path.dirname(__file__),
720
162
"templates/project_fragment.html")
723
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
724
settmpl = self._loader.load(set_fragment)
164
for projectset in self.context.project_sets:
165
settmpl = loader.load(set_fragment)
725
166
setCtx = Context()
727
167
setCtx['projectset'] = projectset
168
setCtx['new_project_url'] = self.new_project_url(projectset)
728
169
setCtx['projects'] = []
729
setCtx['GroupsView'] = GroupsView
730
setCtx['ProjectSetEdit'] = ProjectSetEdit
731
setCtx['ProjectNew'] = ProjectNew
734
projectset.projects.order_by(ivle.database.Project.deadline):
735
projecttmpl = self._loader.load(project_fragment)
171
for project in projectset.projects:
172
projecttmpl = loader.load(project_fragment)
736
173
projectCtx = Context()
737
projectCtx['req'] = req
738
174
projectCtx['project'] = project
739
projectCtx['ProjectEdit'] = ProjectEdit
740
projectCtx['ProjectDelete'] = ProjectDelete
175
projectCtx['project_url'] = self.project_url(projectset, project)
742
177
setCtx['projects'].append(
743
178
projecttmpl.generate(projectCtx))
748
183
class ProjectView(XHTMLView):
749
184
"""View the submissions for a ProjectSet"""
750
185
template = "templates/project.html"
751
permission = "view_project_submissions"
189
def build_subversion_url(self, svnroot, submission):
190
princ = submission.assessed.principal
192
if isinstance(princ, User):
193
path = 'users/%s' % princ.login
195
path = 'groups/%s_%s_%s_%s' % (
196
princ.project_set.offering.subject.short_name,
197
princ.project_set.offering.semester.year,
198
princ.project_set.offering.semester.semester,
201
return urlparse.urljoin(
203
os.path.join(path, submission.path[1:] if
204
submission.path.startswith(os.sep) else
754
207
def populate(self, req, ctx):
755
208
self.plugin_styles[Plugin] = ["project.css"]
758
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
759
ctx['GroupsView'] = GroupsView
760
ctx['EnrolView'] = EnrolView
761
ctx['format_datetime'] = ivle.date.make_date_nice
762
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
763
ctx['project'] = self.context
764
ctx['user'] = req.user
765
ctx['ProjectEdit'] = ProjectEdit
766
ctx['ProjectDelete'] = ProjectDelete
767
ctx['ProjectExport'] = ProjectBashExportView
769
class ProjectBashExportView(TextView):
770
"""Produce a Bash script for exporting projects"""
771
template = "templates/project-export.sh"
772
content_type = "text/x-sh"
773
permission = "view_project_submissions"
775
def populate(self, req, ctx):
777
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
778
ctx['format_datetime'] = ivle.date.make_date_nice
779
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
780
ctx['project'] = self.context
781
ctx['user'] = req.user
782
ctx['now'] = datetime.datetime.now()
783
ctx['format_datetime'] = ivle.date.make_date_nice
784
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
786
class ProjectUniquenessValidator(formencode.FancyValidator):
787
"""A FormEncode validator that checks that a project short_name is unique
790
The project referenced by state.existing_project is permitted to
791
hold that short_name. If any other project holds it, the input is rejected.
793
def _to_python(self, value, state):
794
if (state.store.find(
796
Project.short_name == unicode(value),
797
Project.project_set_id == ProjectSet.id,
798
ProjectSet.offering == state.offering).one() not in
799
(None, state.existing_project)):
800
raise formencode.Invalid(
801
"A project with that URL name already exists in this offering."
805
class ProjectSchema(formencode.Schema):
806
name = formencode.validators.UnicodeString(not_empty=True)
807
short_name = formencode.All(
808
URLNameValidator(not_empty=True),
809
ProjectUniquenessValidator())
810
deadline = DateTimeValidator(not_empty=True)
811
url = formencode.validators.URL(if_missing=None, not_empty=False)
812
synopsis = formencode.validators.UnicodeString(not_empty=True)
814
class ProjectEdit(BaseFormView):
815
"""A form to edit a project."""
816
template = 'templates/project-edit.html'
822
return ProjectSchema()
824
def populate(self, req, ctx):
825
super(ProjectEdit, self).populate(req, ctx)
826
ctx['projectset'] = self.context.project_set
828
def populate_state(self, state):
829
state.offering = self.context.project_set.offering
830
state.existing_project = self.context
832
def get_default_data(self, req):
834
'name': self.context.name,
835
'short_name': self.context.short_name,
836
'deadline': self.context.deadline,
837
'url': self.context.url,
838
'synopsis': self.context.synopsis,
841
def save_object(self, req, data):
842
self.context.name = data['name']
843
self.context.short_name = data['short_name']
844
self.context.deadline = data['deadline']
845
self.context.url = unicode(data['url']) if data['url'] else None
846
self.context.synopsis = data['synopsis']
849
class ProjectNew(BaseFormView):
850
"""A form to create a new project."""
851
template = 'templates/project-new.html'
857
return ProjectSchema()
859
def populate(self, req, ctx):
860
super(ProjectNew, self).populate(req, ctx)
861
ctx['projectset'] = self.context
863
def populate_state(self, state):
864
state.offering = self.context.offering
865
state.existing_project = None
867
def get_default_data(self, req):
870
def save_object(self, req, data):
871
new_project = Project()
872
new_project.project_set = self.context
873
new_project.name = data['name']
874
new_project.short_name = data['short_name']
875
new_project.deadline = data['deadline']
876
new_project.url = unicode(data['url']) if data['url'] else None
877
new_project.synopsis = data['synopsis']
878
req.store.add(new_project)
881
class ProjectDelete(XHTMLView):
882
"""A form to delete a project."""
883
template = 'templates/project-delete.html'
887
def populate(self, req, ctx):
888
# If post, delete the project, or display a message explaining that
889
# the project cannot be deleted
890
if self.context.can_delete:
891
if req.method == 'POST':
892
self.context.delete()
893
self.template = 'templates/project-deleted.html'
896
self.template = 'templates/project-undeletable.html'
898
# If get and can delete, display a delete confirmation page
900
# Variables for the template
902
ctx['project'] = self.context
903
ctx['OfferingProjectsView'] = OfferingProjectsView
905
class ProjectSetSchema(formencode.Schema):
906
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
908
class ProjectSetEdit(BaseFormView):
909
"""A form to edit a project set."""
910
template = 'templates/projectset-edit.html'
916
return ProjectSetSchema()
918
def populate(self, req, ctx):
919
super(ProjectSetEdit, self).populate(req, ctx)
921
def get_default_data(self, req):
923
'group_size': self.context.max_students_per_group,
926
def save_object(self, req, data):
927
self.context.max_students_per_group = data['group_size']
930
class ProjectSetNew(BaseFormView):
931
"""A form to create a new project set."""
932
template = 'templates/projectset-new.html'
935
breadcrumb_text = "Projects"
939
return ProjectSetSchema()
941
def populate(self, req, ctx):
942
super(ProjectSetNew, self).populate(req, ctx)
944
def get_default_data(self, req):
947
def save_object(self, req, data):
948
new_set = ProjectSet()
949
new_set.offering = self.context
950
new_set.max_students_per_group = data['group_size']
951
req.store.add(new_set)
210
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
211
ctx['build_subversion_url'] = self.build_subversion_url
212
ctx['svn_addr'] = req.config['urls']['svn_addr']
213
ctx['project'] = self.context
214
ctx['user'] = req.user
216
class OfferingEnrolmentSet(object):
217
def __init__(self, offering):
218
self.offering = offering
954
220
class Plugin(ViewPlugin, MediaPlugin):
955
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
956
offering_to_project, offering_to_projectset,
957
offering_to_enrolment)
959
subject_url, semester_url, offering_url, projectset_url, project_url,
221
forward_routes = (root_to_subject, subject_to_offering,
222
offering_to_project, offering_to_projectset)
223
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
962
225
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
963
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
964
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
965
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
966
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
967
(Subject, '+index', SubjectView),
968
(Subject, '+edit', SubjectEdit),
969
(Subject, '+new-offering', SubjectOfferingNew),
970
(Semester, '+edit', SemesterEdit),
971
(Offering, '+index', OfferingView),
972
(Offering, '+edit', OfferingEdit),
973
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
974
(Offering, ('+enrolments', '+index'), EnrolmentsView),
975
226
(Offering, ('+enrolments', '+new'), EnrolView),
976
(Enrolment, '+edit', EnrolmentEdit),
977
(Enrolment, '+delete', EnrolmentDelete),
978
227
(Offering, ('+projects', '+index'), OfferingProjectsView),
979
(Offering, ('+projects', '+new-set'), ProjectSetNew),
980
(ProjectSet, '+edit', ProjectSetEdit),
981
(ProjectSet, '+new', ProjectNew),
982
228
(Project, '+index', ProjectView),
983
(Project, '+edit', ProjectEdit),
984
(Project, '+delete', ProjectDelete),
985
(Project, ('+export', 'project-export.sh'),
986
ProjectBashExportView),
230
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
231
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
232
(Project, '+index', ProjectRESTView, 'api'),
989
breadcrumbs = {Subject: SubjectBreadcrumb,
990
Offering: OfferingBreadcrumb,
991
User: UserBreadcrumb,
992
Project: ProjectBreadcrumb,
993
Enrolment: EnrolmentBreadcrumb,
997
236
('subjects', 'Subjects',
998
237
'View subject content and complete worksheets',