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, svnroot, submission):
739
princ = submission.assessed.principal
741
if isinstance(princ, User):
742
path = 'users/%s' % princ.login
744
path = 'groups/%s_%s_%s_%s' % (
745
princ.project_set.offering.subject.short_name,
746
princ.project_set.offering.semester.year,
747
princ.project_set.offering.semester.semester,
750
return urlparse.urljoin(
752
os.path.join(path, submission.path[1:] if
753
submission.path.startswith(os.sep) else
756
def populate(self, req, ctx):
757
self.plugin_styles[Plugin] = ["project.css"]
760
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
761
ctx['GroupsView'] = GroupsView
762
ctx['EnrolView'] = EnrolView
763
ctx['format_datetime'] = ivle.date.make_date_nice
764
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
765
ctx['build_subversion_url'] = self.build_subversion_url
766
ctx['svn_addr'] = req.config['urls']['svn_addr']
767
ctx['project'] = self.context
768
ctx['user'] = req.user
769
ctx['ProjectEdit'] = ProjectEdit
770
ctx['ProjectDelete'] = ProjectDelete
772
class ProjectUniquenessValidator(formencode.FancyValidator):
773
"""A FormEncode validator that checks that a project short_name is unique
776
The project referenced by state.existing_project is permitted to
777
hold that short_name. If any other project holds it, the input is rejected.
779
def _to_python(self, value, state):
780
if (state.store.find(
782
Project.short_name == unicode(value),
783
Project.project_set_id == ProjectSet.id,
784
ProjectSet.offering == state.offering).one() not in
785
(None, state.existing_project)):
786
raise formencode.Invalid(
787
"A project with that URL name already exists in this offering."
791
class ProjectSchema(formencode.Schema):
792
name = formencode.validators.UnicodeString(not_empty=True)
793
short_name = formencode.All(
794
URLNameValidator(not_empty=True),
795
ProjectUniquenessValidator())
796
deadline = DateTimeValidator(not_empty=True)
797
url = formencode.validators.URL(if_missing=None, not_empty=False)
798
synopsis = formencode.validators.UnicodeString(not_empty=True)
800
class ProjectEdit(BaseFormView):
801
"""A form to edit a project."""
802
template = 'templates/project-edit.html'
808
return ProjectSchema()
810
def populate(self, req, ctx):
811
super(ProjectEdit, self).populate(req, ctx)
812
ctx['projectset'] = self.context.project_set
814
def populate_state(self, state):
815
state.offering = self.context.project_set.offering
816
state.existing_project = self.context
818
def get_default_data(self, req):
820
'name': self.context.name,
821
'short_name': self.context.short_name,
822
'deadline': self.context.deadline,
823
'url': self.context.url,
824
'synopsis': self.context.synopsis,
827
def save_object(self, req, data):
828
self.context.name = data['name']
829
self.context.short_name = data['short_name']
830
self.context.deadline = data['deadline']
831
self.context.url = unicode(data['url']) if data['url'] else None
832
self.context.synopsis = data['synopsis']
835
class ProjectNew(BaseFormView):
836
"""A form to create a new project."""
837
template = 'templates/project-new.html'
843
return ProjectSchema()
845
def populate(self, req, ctx):
846
super(ProjectNew, self).populate(req, ctx)
847
ctx['projectset'] = self.context
849
def populate_state(self, state):
850
state.offering = self.context.offering
851
state.existing_project = None
853
def get_default_data(self, req):
856
def save_object(self, req, data):
857
new_project = Project()
858
new_project.project_set = self.context
859
new_project.name = data['name']
860
new_project.short_name = data['short_name']
861
new_project.deadline = data['deadline']
862
new_project.url = unicode(data['url']) if data['url'] else None
863
new_project.synopsis = data['synopsis']
864
req.store.add(new_project)
867
class ProjectDelete(XHTMLView):
868
"""A form to delete a project."""
869
template = 'templates/project-delete.html'
873
def populate(self, req, ctx):
874
# If post, delete the project, or display a message explaining that
875
# the project cannot be deleted
876
if self.context.can_delete:
877
if req.method == 'POST':
878
self.context.delete()
879
self.template = 'templates/project-deleted.html'
882
self.template = 'templates/project-undeletable.html'
884
# If get and can delete, display a delete confirmation page
886
# Variables for the template
888
ctx['project'] = self.context
889
ctx['OfferingProjectsView'] = OfferingProjectsView
891
class ProjectSetSchema(formencode.Schema):
892
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
894
class ProjectSetEdit(BaseFormView):
895
"""A form to edit a project set."""
896
template = 'templates/projectset-edit.html'
902
return ProjectSetSchema()
904
def populate(self, req, ctx):
905
super(ProjectSetEdit, self).populate(req, ctx)
907
def get_default_data(self, req):
909
'group_size': self.context.max_students_per_group,
912
def save_object(self, req, data):
913
self.context.max_students_per_group = data['group_size']
916
class ProjectSetNew(BaseFormView):
917
"""A form to create a new project set."""
918
template = 'templates/projectset-new.html'
921
breadcrumb_text = "Projects"
925
return ProjectSetSchema()
927
def populate(self, req, ctx):
928
super(ProjectSetNew, self).populate(req, ctx)
930
def get_default_data(self, req):
933
def save_object(self, req, data):
934
new_set = ProjectSet()
935
new_set.offering = self.context
936
new_set.max_students_per_group = data['group_size']
937
req.store.add(new_set)
940
class Plugin(ViewPlugin, MediaPlugin):
941
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
942
offering_to_project, offering_to_projectset,
943
offering_to_enrolment)
945
subject_url, semester_url, offering_url, projectset_url, project_url,
948
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
949
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
950
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
951
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
952
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
953
(Subject, '+index', SubjectView),
954
(Subject, '+edit', SubjectEdit),
955
(Subject, '+new-offering', SubjectOfferingNew),
956
(Semester, '+edit', SemesterEdit),
957
(Offering, '+index', OfferingView),
958
(Offering, '+edit', OfferingEdit),
959
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
960
(Offering, ('+enrolments', '+index'), EnrolmentsView),
961
(Offering, ('+enrolments', '+new'), EnrolView),
962
(Enrolment, '+edit', EnrolmentEdit),
963
(Enrolment, '+delete', EnrolmentDelete),
964
(Offering, ('+projects', '+index'), OfferingProjectsView),
965
(Offering, ('+projects', '+new-set'), ProjectSetNew),
966
(ProjectSet, '+edit', ProjectSetEdit),
967
(ProjectSet, '+new', ProjectNew),
968
(Project, '+index', ProjectView),
969
(Project, '+edit', ProjectEdit),
970
(Project, '+delete', ProjectDelete),
973
breadcrumbs = {Subject: SubjectBreadcrumb,
974
Offering: OfferingBreadcrumb,
975
User: UserBreadcrumb,
976
Project: ProjectBreadcrumb,
977
Enrolment: EnrolmentBreadcrumb,
981
('subjects', 'Subjects',
982
'View subject content and complete worksheets',
983
'subjects.png', 'subjects', 5)
986
media = 'subject-media'