2
# Copyright (C) 2007-2008 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22
# This is an IVLE application.
23
# A sample / testing application for IVLE.
31
from storm.locals import Desc, Store
33
from genshi.filters import HTMLFormFiller
34
from genshi.template import Context
36
import formencode.validators
38
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
40
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
45
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
46
ProjectSet, Project, ProjectSubmission
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
62
class SubjectsView(XHTMLView):
63
'''The view of the list of subjects.'''
64
template = 'templates/subjects.html'
66
breadcrumb_text = "Subjects"
68
def authorize(self, req):
69
return req.user is not None
71
def populate(self, req, ctx):
73
ctx['user'] = req.user
76
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
77
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
305
# As we go, calculate the total score for this subject
306
# (Assessable worksheets only, mandatory problems only)
308
ctx['worksheets'], problems_total, problems_done = (
309
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
310
req.config, req.store, req.user, self.context))
312
ctx['exercises_total'] = problems_total
313
ctx['exercises_done'] = problems_done
314
if problems_total > 0:
315
if problems_done >= problems_total:
316
ctx['worksheets_complete_class'] = "complete"
317
elif problems_done > 0:
318
ctx['worksheets_complete_class'] = "semicomplete"
320
ctx['worksheets_complete_class'] = "incomplete"
321
# Calculate the final percentage and mark for the subject
322
(ctx['exercises_pct'], ctx['worksheet_mark'],
323
ctx['worksheet_max_mark']) = (
324
ivle.worksheet.utils.calculate_mark(
325
problems_done, problems_total))
328
class SubjectValidator(formencode.FancyValidator):
329
"""A FormEncode validator that turns a subject name into a subject.
331
The state must have a 'store' attribute, which is the Storm store
334
def _to_python(self, value, state):
335
subject = state.store.find(Subject, short_name=value).one()
339
raise formencode.Invalid('Subject does not exist', value, state)
342
class SemesterValidator(formencode.FancyValidator):
343
"""A FormEncode validator that turns a string into a semester.
345
The string should be of the form 'year/semester', eg. '2009/1'.
347
The state must have a 'store' attribute, which is the Storm store
350
def _to_python(self, value, state):
352
year, semester = value.split('/')
354
year = semester = None
356
semester = state.store.find(
357
Semester, year=year, semester=semester).one()
361
raise formencode.Invalid('Semester does not exist', value, state)
364
class OfferingUniquenessValidator(formencode.FancyValidator):
365
"""A FormEncode validator that checks that an offering is unique.
367
There cannot be more than one offering in the same year and semester.
369
The offering referenced by state.existing_offering is permitted to
370
hold that year and semester tuple. If any other object holds it, the
373
def _to_python(self, value, state):
374
if (state.store.find(
375
Offering, subject=value['subject'],
376
semester=value['semester']).one() not in
377
(None, state.existing_offering)):
378
raise formencode.Invalid(
379
'Offering already exists', value, state)
383
class OfferingSchema(formencode.Schema):
384
description = formencode.validators.UnicodeString(
385
if_missing=None, not_empty=False)
386
url = formencode.validators.URL(if_missing=None, not_empty=False)
387
show_worksheet_marks = formencode.validators.StringBoolean(
391
class OfferingAdminSchema(OfferingSchema):
392
subject = formencode.All(
393
SubjectValidator(), formencode.validators.UnicodeString())
394
semester = formencode.All(
395
SemesterValidator(), formencode.validators.UnicodeString())
396
chained_validators = [OfferingUniquenessValidator()]
399
class OfferingEdit(BaseFormView):
400
"""A form to edit an offering's details."""
401
template = 'templates/offering-edit.html'
407
if self.req.user.admin:
408
return OfferingAdminSchema()
410
return OfferingSchema()
412
def populate(self, req, ctx):
413
super(OfferingEdit, self).populate(req, ctx)
414
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
415
ctx['semesters'] = req.store.find(Semester).order_by(
416
Semester.year, Semester.semester)
417
ctx['force_subject'] = None
419
def populate_state(self, state):
420
state.existing_offering = self.context
422
def get_default_data(self, req):
424
'subject': self.context.subject.short_name,
425
'semester': self.context.semester.year + '/' +
426
self.context.semester.semester,
427
'url': self.context.url,
428
'description': self.context.description,
429
'show_worksheet_marks': self.context.show_worksheet_marks,
432
def save_object(self, req, data):
434
self.context.subject = data['subject']
435
self.context.semester = data['semester']
436
self.context.description = data['description']
437
self.context.url = unicode(data['url']) if data['url'] else None
438
self.context.show_worksheet_marks = data['show_worksheet_marks']
442
class OfferingNew(BaseFormView):
443
"""A form to create an offering."""
444
template = 'templates/offering-new.html'
447
def authorize(self, req):
448
return req.user is not None and req.user.admin
452
return OfferingAdminSchema()
454
def populate(self, req, ctx):
455
super(OfferingNew, self).populate(req, ctx)
456
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
457
ctx['semesters'] = req.store.find(Semester).order_by(
458
Semester.year, Semester.semester)
459
ctx['force_subject'] = None
461
def populate_state(self, state):
462
state.existing_offering = None
464
def get_default_data(self, req):
467
def save_object(self, req, data):
468
new_offering = Offering()
469
new_offering.subject = data['subject']
470
new_offering.semester = data['semester']
471
new_offering.description = data['description']
472
new_offering.url = unicode(data['url']) if data['url'] else None
473
new_offering.show_worksheet_marks = data['show_worksheet_marks']
475
req.store.add(new_offering)
478
class SubjectOfferingNew(OfferingNew):
479
"""A form to create an offering for a given subject."""
480
# Identical to OfferingNew, except it forces the subject to be the subject
482
def populate(self, req, ctx):
483
super(SubjectOfferingNew, self).populate(req, ctx)
484
ctx['force_subject'] = self.context
486
class OfferingCloneWorksheetsSchema(formencode.Schema):
487
subject = formencode.All(
488
SubjectValidator(), formencode.validators.UnicodeString())
489
semester = formencode.All(
490
SemesterValidator(), formencode.validators.UnicodeString())
493
class OfferingCloneWorksheets(BaseFormView):
494
"""A form to clone worksheets from one offering to another."""
495
template = 'templates/offering-clone-worksheets.html'
498
def authorize(self, req):
499
return req.user is not None and req.user.admin
503
return OfferingCloneWorksheetsSchema()
505
def populate(self, req, ctx):
506
super(OfferingCloneWorksheets, self).populate(req, ctx)
507
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
508
ctx['semesters'] = req.store.find(Semester).order_by(
509
Semester.year, Semester.semester)
511
def get_default_data(self, req):
514
def save_object(self, req, data):
515
if self.context.worksheets.count() > 0:
517
"Cannot clone to target with existing worksheets.")
518
offering = req.store.find(
519
Offering, subject=data['subject'], semester=data['semester']).one()
521
raise BadRequest("No such offering.")
522
if offering.worksheets.count() == 0:
523
raise BadRequest("Source offering has no worksheets.")
525
self.context.clone_worksheets(offering)
529
class UserValidator(formencode.FancyValidator):
530
"""A FormEncode validator that turns a username into a user.
532
The state must have a 'store' attribute, which is the Storm store
534
def _to_python(self, value, state):
535
user = User.get_by_login(state.store, value)
539
raise formencode.Invalid('User does not exist', value, state)
542
class NoEnrolmentValidator(formencode.FancyValidator):
543
"""A FormEncode validator that ensures absence of an enrolment.
545
The state must have an 'offering' attribute.
547
def _to_python(self, value, state):
548
if state.offering.get_enrolment(value):
549
raise formencode.Invalid('User already enrolled', value, state)
553
class RoleEnrolmentValidator(formencode.FancyValidator):
554
"""A FormEncode validator that checks permission to enrol users with a
557
The state must have an 'offering' attribute.
559
def _to_python(self, value, state):
560
if (("enrol_" + value) not in
561
state.offering.get_permissions(state.user, state.config)):
562
raise formencode.Invalid('Not allowed to assign users that role',
567
class EnrolSchema(formencode.Schema):
568
user = formencode.All(NoEnrolmentValidator(), UserValidator())
569
role = formencode.All(formencode.validators.OneOf(
570
["lecturer", "tutor", "student"]),
571
RoleEnrolmentValidator(),
572
formencode.validators.UnicodeString())
575
class EnrolmentsView(XHTMLView):
576
"""A page which displays all users enrolled in an offering."""
577
template = 'templates/enrolments.html'
580
breadcrumb_text = 'Enrolments'
582
def populate(self, req, ctx):
584
ctx['offering'] = self.context
585
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
586
ctx['offering_perms'] = self.context.get_permissions(
587
req.user, req.config)
588
ctx['EnrolView'] = EnrolView
589
ctx['EnrolmentEdit'] = EnrolmentEdit
590
ctx['EnrolmentDelete'] = EnrolmentDelete
593
class EnrolView(XHTMLView):
594
"""A form to enrol a user in an offering."""
595
template = 'templates/enrol.html'
599
def filter(self, stream, ctx):
600
return stream | HTMLFormFiller(data=ctx['data'])
602
def populate(self, req, ctx):
603
if req.method == 'POST':
604
data = dict(req.get_fieldstorage())
606
validator = EnrolSchema()
607
req.offering = self.context # XXX: Getting into state.
608
data = validator.to_python(data, state=req)
609
self.context.enrol(data['user'], data['role'])
611
req.throw_redirect(req.uri)
612
except formencode.Invalid, e:
613
errors = e.unpack_errors()
618
ctx['data'] = data or {}
619
ctx['offering'] = self.context
620
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
621
ctx['errors'] = errors
622
# If all of the fields validated, set the global form error.
623
if isinstance(errors, basestring):
624
ctx['error_value'] = errors
627
class EnrolmentEditSchema(formencode.Schema):
628
role = formencode.All(formencode.validators.OneOf(
629
["lecturer", "tutor", "student"]),
630
RoleEnrolmentValidator(),
631
formencode.validators.UnicodeString())
634
class EnrolmentEdit(BaseFormView):
635
"""A form to alter an enrolment's role."""
636
template = 'templates/enrolment-edit.html'
640
def populate_state(self, state):
641
state.offering = self.context.offering
643
def get_default_data(self, req):
644
return {'role': self.context.role}
648
return EnrolmentEditSchema()
650
def save_object(self, req, data):
651
self.context.role = data['role']
653
def get_return_url(self, obj):
654
return self.req.publisher.generate(
655
self.context.offering, EnrolmentsView)
657
def populate(self, req, ctx):
658
super(EnrolmentEdit, self).populate(req, ctx)
659
ctx['offering_perms'] = self.context.offering.get_permissions(
660
req.user, req.config)
663
class EnrolmentDelete(XHTMLView):
664
"""A form to alter an enrolment's role."""
665
template = 'templates/enrolment-delete.html'
669
def populate(self, req, ctx):
670
# If POSTing, delete delete delete.
671
if req.method == 'POST':
672
self.context.delete()
674
req.throw_redirect(req.publisher.generate(
675
self.context.offering, EnrolmentsView))
677
ctx['enrolment'] = self.context
680
class OfferingProjectsView(XHTMLView):
681
"""View the projects for an offering."""
682
template = 'templates/offering_projects.html'
685
breadcrumb_text = 'Projects'
687
def populate(self, req, ctx):
688
self.plugin_styles[Plugin] = ["project.css"]
690
ctx['offering'] = self.context
691
ctx['projectsets'] = []
693
#Open the projectset Fragment, and render it for inclusion
694
#into the ProjectSets page
695
set_fragment = os.path.join(os.path.dirname(__file__),
696
"templates/projectset_fragment.html")
697
project_fragment = os.path.join(os.path.dirname(__file__),
698
"templates/project_fragment.html")
701
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
702
settmpl = self._loader.load(set_fragment)
705
setCtx['projectset'] = projectset
706
setCtx['projects'] = []
707
setCtx['GroupsView'] = GroupsView
708
setCtx['ProjectSetEdit'] = ProjectSetEdit
709
setCtx['ProjectNew'] = ProjectNew
712
projectset.projects.order_by(ivle.database.Project.deadline):
713
projecttmpl = self._loader.load(project_fragment)
714
projectCtx = Context()
715
projectCtx['req'] = req
716
projectCtx['project'] = project
717
projectCtx['ProjectEdit'] = ProjectEdit
718
projectCtx['ProjectDelete'] = ProjectDelete
720
setCtx['projects'].append(
721
projecttmpl.generate(projectCtx))
723
ctx['projectsets'].append(settmpl.generate(setCtx))
726
class ProjectView(XHTMLView):
727
"""View the submissions for a ProjectSet"""
728
template = "templates/project.html"
729
permission = "view_project_submissions"
732
def build_subversion_url(self, svnroot, submission):
733
princ = submission.assessed.principal
735
if isinstance(princ, User):
736
path = 'users/%s' % princ.login
738
path = 'groups/%s_%s_%s_%s' % (
739
princ.project_set.offering.subject.short_name,
740
princ.project_set.offering.semester.year,
741
princ.project_set.offering.semester.semester,
744
return urlparse.urljoin(
746
os.path.join(path, submission.path[1:] if
747
submission.path.startswith(os.sep) else
750
def populate(self, req, ctx):
751
self.plugin_styles[Plugin] = ["project.css"]
754
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
755
ctx['GroupsView'] = GroupsView
756
ctx['EnrolView'] = EnrolView
757
ctx['format_datetime'] = ivle.date.make_date_nice
758
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
759
ctx['build_subversion_url'] = self.build_subversion_url
760
ctx['svn_addr'] = req.config['urls']['svn_addr']
761
ctx['project'] = self.context
762
ctx['user'] = req.user
763
ctx['ProjectEdit'] = ProjectEdit
764
ctx['ProjectDelete'] = ProjectDelete
766
class ProjectUniquenessValidator(formencode.FancyValidator):
767
"""A FormEncode validator that checks that a project short_name is unique
770
The project referenced by state.existing_project is permitted to
771
hold that short_name. If any other project holds it, the input is rejected.
773
def _to_python(self, value, state):
774
if (state.store.find(
776
Project.short_name == unicode(value),
777
Project.project_set_id == ProjectSet.id,
778
ProjectSet.offering == state.offering).one() not in
779
(None, state.existing_project)):
780
raise formencode.Invalid(
781
"A project with that URL name already exists in this offering."
785
class ProjectSchema(formencode.Schema):
786
name = formencode.validators.UnicodeString(not_empty=True)
787
short_name = formencode.All(
788
URLNameValidator(not_empty=True),
789
ProjectUniquenessValidator())
790
deadline = DateTimeValidator(not_empty=True)
791
url = formencode.validators.URL(if_missing=None, not_empty=False)
792
synopsis = formencode.validators.UnicodeString(not_empty=True)
794
class ProjectEdit(BaseFormView):
795
"""A form to edit a project."""
796
template = 'templates/project-edit.html'
802
return ProjectSchema()
804
def populate(self, req, ctx):
805
super(ProjectEdit, self).populate(req, ctx)
806
ctx['projectset'] = self.context.project_set
808
def populate_state(self, state):
809
state.offering = self.context.project_set.offering
810
state.existing_project = self.context
812
def get_default_data(self, req):
814
'name': self.context.name,
815
'short_name': self.context.short_name,
816
'deadline': self.context.deadline,
817
'url': self.context.url,
818
'synopsis': self.context.synopsis,
821
def save_object(self, req, data):
822
self.context.name = data['name']
823
self.context.short_name = data['short_name']
824
self.context.deadline = data['deadline']
825
self.context.url = unicode(data['url']) if data['url'] else None
826
self.context.synopsis = data['synopsis']
829
class ProjectNew(BaseFormView):
830
"""A form to create a new project."""
831
template = 'templates/project-new.html'
837
return ProjectSchema()
839
def populate(self, req, ctx):
840
super(ProjectNew, self).populate(req, ctx)
841
ctx['projectset'] = self.context
843
def populate_state(self, state):
844
state.offering = self.context.offering
845
state.existing_project = None
847
def get_default_data(self, req):
850
def save_object(self, req, data):
851
new_project = Project()
852
new_project.project_set = self.context
853
new_project.name = data['name']
854
new_project.short_name = data['short_name']
855
new_project.deadline = data['deadline']
856
new_project.url = unicode(data['url']) if data['url'] else None
857
new_project.synopsis = data['synopsis']
858
req.store.add(new_project)
861
class ProjectDelete(XHTMLView):
862
"""A form to delete a project."""
863
template = 'templates/project-delete.html'
867
def populate(self, req, ctx):
868
# If post, delete the project, or display a message explaining that
869
# the project cannot be deleted
870
if self.context.can_delete:
871
if req.method == 'POST':
872
self.context.delete()
873
self.template = 'templates/project-deleted.html'
876
self.template = 'templates/project-undeletable.html'
878
# If get and can delete, display a delete confirmation page
880
# Variables for the template
882
ctx['project'] = self.context
883
ctx['OfferingProjectsView'] = OfferingProjectsView
885
class ProjectSetSchema(formencode.Schema):
886
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
888
class ProjectSetEdit(BaseFormView):
889
"""A form to edit a project set."""
890
template = 'templates/projectset-edit.html'
896
return ProjectSetSchema()
898
def populate(self, req, ctx):
899
super(ProjectSetEdit, self).populate(req, ctx)
901
def get_default_data(self, req):
903
'group_size': self.context.max_students_per_group,
906
def save_object(self, req, data):
907
self.context.max_students_per_group = data['group_size']
910
class ProjectSetNew(BaseFormView):
911
"""A form to create a new project set."""
912
template = 'templates/projectset-new.html'
915
breadcrumb_text = "Projects"
919
return ProjectSetSchema()
921
def populate(self, req, ctx):
922
super(ProjectSetNew, self).populate(req, ctx)
924
def get_default_data(self, req):
927
def save_object(self, req, data):
928
new_set = ProjectSet()
929
new_set.offering = self.context
930
new_set.max_students_per_group = data['group_size']
931
req.store.add(new_set)
934
class Plugin(ViewPlugin, MediaPlugin):
935
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
936
offering_to_project, offering_to_projectset,
937
offering_to_enrolment)
939
subject_url, semester_url, offering_url, projectset_url, project_url,
942
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
943
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
944
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
945
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
946
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
947
(Subject, '+index', SubjectView),
948
(Subject, '+edit', SubjectEdit),
949
(Subject, '+new-offering', SubjectOfferingNew),
950
(Semester, '+edit', SemesterEdit),
951
(Offering, '+index', OfferingView),
952
(Offering, '+edit', OfferingEdit),
953
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
954
(Offering, ('+enrolments', '+index'), EnrolmentsView),
955
(Offering, ('+enrolments', '+new'), EnrolView),
956
(Enrolment, '+edit', EnrolmentEdit),
957
(Enrolment, '+delete', EnrolmentDelete),
958
(Offering, ('+projects', '+index'), OfferingProjectsView),
959
(Offering, ('+projects', '+new-set'), ProjectSetNew),
960
(ProjectSet, '+edit', ProjectSetEdit),
961
(ProjectSet, '+new', ProjectNew),
962
(Project, '+index', ProjectView),
963
(Project, '+edit', ProjectEdit),
964
(Project, '+delete', ProjectDelete),
967
breadcrumbs = {Subject: SubjectBreadcrumb,
968
Offering: OfferingBreadcrumb,
969
User: UserBreadcrumb,
970
Project: ProjectBreadcrumb,
971
Enrolment: EnrolmentBreadcrumb,
975
('subjects', 'Subjects',
976
'View subject content and complete worksheets',
977
'subjects.png', 'subjects', 5)
980
media = 'subject-media'