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
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)
535
class UserValidator(formencode.FancyValidator):
536
"""A FormEncode validator that turns a username into a user.
538
The state must have a 'store' attribute, which is the Storm store
540
def _to_python(self, value, state):
541
user = User.get_by_login(state.store, value)
545
raise formencode.Invalid('User does not exist', value, state)
548
class NoEnrolmentValidator(formencode.FancyValidator):
549
"""A FormEncode validator that ensures absence of an enrolment.
551
The state must have an 'offering' attribute.
553
def _to_python(self, value, state):
554
if state.offering.get_enrolment(value):
555
raise formencode.Invalid('User already enrolled', value, state)
559
class RoleEnrolmentValidator(formencode.FancyValidator):
560
"""A FormEncode validator that checks permission to enrol users with a
563
The state must have an 'offering' attribute.
565
def _to_python(self, value, state):
566
if (("enrol_" + value) not in
567
state.offering.get_permissions(state.user, state.config)):
568
raise formencode.Invalid('Not allowed to assign users that role',
573
class EnrolSchema(formencode.Schema):
574
user = formencode.All(NoEnrolmentValidator(), UserValidator())
575
role = formencode.All(formencode.validators.OneOf(
576
["lecturer", "tutor", "student"]),
577
RoleEnrolmentValidator(),
578
formencode.validators.UnicodeString())
581
class EnrolmentsView(XHTMLView):
582
"""A page which displays all users enrolled in an offering."""
583
template = 'templates/enrolments.html'
586
breadcrumb_text = 'Enrolments'
588
def populate(self, req, ctx):
590
ctx['offering'] = self.context
591
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
592
ctx['offering_perms'] = self.context.get_permissions(
593
req.user, req.config)
594
ctx['EnrolView'] = EnrolView
595
ctx['EnrolmentEdit'] = EnrolmentEdit
596
ctx['EnrolmentDelete'] = EnrolmentDelete
599
class EnrolView(XHTMLView):
600
"""A form to enrol a user in an offering."""
601
template = 'templates/enrol.html'
605
def filter(self, stream, ctx):
606
return stream | HTMLFormFiller(data=ctx['data'])
608
def populate(self, req, ctx):
609
if req.method == 'POST':
610
data = dict(req.get_fieldstorage())
612
validator = EnrolSchema()
613
req.offering = self.context # XXX: Getting into state.
614
data = validator.to_python(data, state=req)
615
self.context.enrol(data['user'], data['role'])
617
req.throw_redirect(req.uri)
618
except formencode.Invalid, e:
619
errors = e.unpack_errors()
624
ctx['data'] = data or {}
625
ctx['offering'] = self.context
626
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
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
686
class OfferingProjectsView(XHTMLView):
687
"""View the projects for an offering."""
688
template = 'templates/offering_projects.html'
691
breadcrumb_text = 'Projects'
693
def populate(self, req, ctx):
694
self.plugin_styles[Plugin] = ["project.css"]
696
ctx['offering'] = self.context
697
ctx['projectsets'] = []
699
#Open the projectset Fragment, and render it for inclusion
700
#into the ProjectSets page
701
set_fragment = os.path.join(os.path.dirname(__file__),
702
"templates/projectset_fragment.html")
703
project_fragment = os.path.join(os.path.dirname(__file__),
704
"templates/project_fragment.html")
707
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
708
settmpl = self._loader.load(set_fragment)
711
setCtx['projectset'] = projectset
712
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)
720
projectCtx = Context()
721
projectCtx['req'] = req
722
projectCtx['project'] = project
723
projectCtx['ProjectEdit'] = ProjectEdit
724
projectCtx['ProjectDelete'] = ProjectDelete
726
setCtx['projects'].append(
727
projecttmpl.generate(projectCtx))
729
ctx['projectsets'].append(settmpl.generate(setCtx))
732
class ProjectView(XHTMLView):
733
"""View the submissions for a ProjectSet"""
734
template = "templates/project.html"
735
permission = "view_project_submissions"
738
def build_subversion_url(self, req, submission):
739
princ = submission.assessed.principal
741
return os.path.join(princ.get_svn_url(req.config, req),
742
submission.path[1:] if
743
submission.path.startswith(os.sep) else
746
def populate(self, req, ctx):
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
754
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
755
ctx['build_subversion_url'] = self.build_subversion_url
756
ctx['project'] = self.context
757
ctx['user'] = req.user
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)
929
class Plugin(ViewPlugin, MediaPlugin):
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,
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),
950
(Offering, ('+enrolments', '+new'), EnrolView),
951
(Enrolment, '+edit', EnrolmentEdit),
952
(Enrolment, '+delete', EnrolmentDelete),
953
(Offering, ('+projects', '+index'), OfferingProjectsView),
954
(Offering, ('+projects', '+new-set'), ProjectSetNew),
955
(ProjectSet, '+edit', ProjectSetEdit),
956
(ProjectSet, '+new', ProjectNew),
957
(Project, '+index', ProjectView),
958
(Project, '+edit', ProjectEdit),
959
(Project, '+delete', ProjectDelete),
962
breadcrumbs = {Subject: SubjectBreadcrumb,
963
Offering: OfferingBreadcrumb,
964
User: UserBreadcrumb,
965
Project: ProjectBreadcrumb,
966
Enrolment: EnrolmentBreadcrumb,
970
('subjects', 'Subjects',
971
'View subject content and complete worksheets',
972
'subjects.png', 'subjects', 5)
975
media = 'subject-media'