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.
32
from storm.locals import Desc, Store
34
from genshi.filters import HTMLFormFiller
35
from genshi.template import Context
37
import formencode.validators
39
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
41
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
42
from ivle.webapp.base.xhtml import XHTMLView
43
from ivle.webapp.base.text import TextView
44
from ivle.webapp.errors import BadRequest
45
from ivle.webapp import ApplicationRoot
47
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
48
ProjectSet, Project, ProjectSubmission
52
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
53
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
64
class SubjectsView(XHTMLView):
65
'''The view of the list of subjects.'''
66
template = 'templates/subjects.html'
68
breadcrumb_text = "Subjects"
70
def authorize(self, req):
71
return req.user is not None
73
def populate(self, req, ctx):
75
ctx['user'] = req.user
78
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
79
Desc(Semester.semester)):
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.semester)
110
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
111
"""A FormEncode validator that checks that a subject name is unused.
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
def __init__(self, matching=None):
117
self.matching = matching
119
def _to_python(self, value, state):
120
if (state.store.find(
121
Subject, short_name=value).one() not in
122
(None, state.existing_subject)):
123
raise formencode.Invalid(
124
'Short name already taken', value, state)
128
class SubjectSchema(formencode.Schema):
129
short_name = formencode.All(
130
SubjectShortNameUniquenessValidator(),
131
URLNameValidator(not_empty=True))
132
name = formencode.validators.UnicodeString(not_empty=True)
133
code = formencode.validators.UnicodeString(not_empty=True)
136
class SubjectFormView(BaseFormView):
137
"""An abstract form to add or edit a subject."""
140
def authorize(self, req):
141
return req.user is not None and req.user.admin
143
def populate_state(self, state):
144
state.existing_subject = None
148
return SubjectSchema()
151
class SubjectNew(SubjectFormView):
152
"""A form to create a subject."""
153
template = 'templates/subject-new.html'
155
def get_default_data(self, req):
158
def save_object(self, req, data):
159
new_subject = Subject()
160
new_subject.short_name = data['short_name']
161
new_subject.name = data['name']
162
new_subject.code = data['code']
164
req.store.add(new_subject)
168
class SubjectEdit(SubjectFormView):
169
"""A form to edit a subject."""
170
template = 'templates/subject-edit.html'
172
def populate_state(self, state):
173
state.existing_subject = self.context
175
def get_default_data(self, req):
177
'short_name': self.context.short_name,
178
'name': self.context.name,
179
'code': self.context.code,
182
def save_object(self, req, data):
183
self.context.short_name = data['short_name']
184
self.context.name = data['name']
185
self.context.code = data['code']
190
class SemesterUniquenessValidator(formencode.FancyValidator):
191
"""A FormEncode validator that checks that a semester is unique.
193
There cannot be more than one semester for the same year and semester.
195
def _to_python(self, value, state):
196
if (state.store.find(
197
Semester, year=value['year'], semester=value['semester']
198
).one() not in (None, state.existing_semester)):
199
raise formencode.Invalid(
200
'Semester already exists', value, state)
204
class SemesterSchema(formencode.Schema):
205
year = URLNameValidator()
206
semester = URLNameValidator()
207
state = formencode.All(
208
formencode.validators.OneOf(["past", "current", "future"]),
209
formencode.validators.UnicodeString())
210
chained_validators = [SemesterUniquenessValidator()]
213
class SemesterFormView(BaseFormView):
216
def authorize(self, req):
217
return req.user is not None and req.user.admin
221
return SemesterSchema()
223
def get_return_url(self, obj):
224
return '/subjects/+manage'
227
class SemesterNew(SemesterFormView):
228
"""A form to create a semester."""
229
template = 'templates/semester-new.html'
232
def populate_state(self, state):
233
state.existing_semester = None
235
def get_default_data(self, req):
238
def save_object(self, req, data):
239
new_semester = Semester()
240
new_semester.year = data['year']
241
new_semester.semester = data['semester']
242
new_semester.state = data['state']
244
req.store.add(new_semester)
248
class SemesterEdit(SemesterFormView):
249
"""A form to edit a semester."""
250
template = 'templates/semester-edit.html'
252
def populate_state(self, state):
253
state.existing_semester = self.context
255
def get_default_data(self, req):
257
'year': self.context.year,
258
'semester': self.context.semester,
259
'state': self.context.state,
262
def save_object(self, req, data):
263
self.context.year = data['year']
264
self.context.semester = data['semester']
265
self.context.state = data['state']
269
class SubjectView(XHTMLView):
270
'''The view of the list of offerings in a given subject.'''
271
template = 'templates/subject.html'
274
def authorize(self, req):
275
return req.user is not None
277
def populate(self, req, ctx):
278
ctx['context'] = self.context
280
ctx['user'] = req.user
281
ctx['offerings'] = list(self.context.offerings)
282
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
283
ctx['SubjectEdit'] = SubjectEdit
284
ctx['SubjectOfferingNew'] = SubjectOfferingNew
287
class OfferingView(XHTMLView):
288
"""The home page of an offering."""
289
template = 'templates/offering.html'
293
def populate(self, req, ctx):
294
# Need the worksheet result styles.
295
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
296
ctx['context'] = self.context
298
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
299
ctx['format_submission_principal'] = util.format_submission_principal
300
ctx['format_datetime'] = ivle.date.make_date_nice
301
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
302
ctx['OfferingEdit'] = OfferingEdit
303
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
304
ctx['GroupsView'] = GroupsView
305
ctx['EnrolmentsView'] = EnrolmentsView
306
ctx['Project'] = ivle.database.Project
308
# As we go, calculate the total score for this subject
309
# (Assessable worksheets only, mandatory problems only)
311
ctx['worksheets'], problems_total, problems_done = (
312
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
313
req.config, req.store, req.user, self.context,
314
as_of=self.context.worksheet_cutoff))
316
ctx['exercises_total'] = problems_total
317
ctx['exercises_done'] = problems_done
318
if problems_total > 0:
319
if problems_done >= problems_total:
320
ctx['worksheets_complete_class'] = "complete"
321
elif problems_done > 0:
322
ctx['worksheets_complete_class'] = "semicomplete"
324
ctx['worksheets_complete_class'] = "incomplete"
325
# Calculate the final percentage and mark for the subject
326
(ctx['exercises_pct'], ctx['worksheet_mark'],
327
ctx['worksheet_max_mark']) = (
328
ivle.worksheet.utils.calculate_mark(
329
problems_done, problems_total))
332
class SubjectValidator(formencode.FancyValidator):
333
"""A FormEncode validator that turns a subject name into a subject.
335
The state must have a 'store' attribute, which is the Storm store
338
def _to_python(self, value, state):
339
subject = state.store.find(Subject, short_name=value).one()
343
raise formencode.Invalid('Subject does not exist', value, state)
346
class SemesterValidator(formencode.FancyValidator):
347
"""A FormEncode validator that turns a string into a semester.
349
The string should be of the form 'year/semester', eg. '2009/1'.
351
The state must have a 'store' attribute, which is the Storm store
354
def _to_python(self, value, state):
356
year, semester = value.split('/')
358
year = semester = None
360
semester = state.store.find(
361
Semester, year=year, semester=semester).one()
365
raise formencode.Invalid('Semester does not exist', value, state)
368
class OfferingUniquenessValidator(formencode.FancyValidator):
369
"""A FormEncode validator that checks that an offering is unique.
371
There cannot be more than one offering in the same year and semester.
373
The offering referenced by state.existing_offering is permitted to
374
hold that year and semester tuple. If any other object holds it, the
377
def _to_python(self, value, state):
378
if (state.store.find(
379
Offering, subject=value['subject'],
380
semester=value['semester']).one() not in
381
(None, state.existing_offering)):
382
raise formencode.Invalid(
383
'Offering already exists', value, state)
387
class OfferingSchema(formencode.Schema):
388
description = formencode.validators.UnicodeString(
389
if_missing=None, not_empty=False)
390
url = formencode.validators.URL(if_missing=None, not_empty=False)
391
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
392
show_worksheet_marks = formencode.validators.StringBoolean(
396
class OfferingAdminSchema(OfferingSchema):
397
subject = formencode.All(
398
SubjectValidator(), formencode.validators.UnicodeString())
399
semester = formencode.All(
400
SemesterValidator(), formencode.validators.UnicodeString())
401
chained_validators = [OfferingUniquenessValidator()]
404
class OfferingEdit(BaseFormView):
405
"""A form to edit an offering's details."""
406
template = 'templates/offering-edit.html'
412
if self.req.user.admin:
413
return OfferingAdminSchema()
415
return OfferingSchema()
417
def populate(self, req, ctx):
418
super(OfferingEdit, self).populate(req, ctx)
419
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
420
ctx['semesters'] = req.store.find(Semester).order_by(
421
Semester.year, Semester.semester)
422
ctx['force_subject'] = None
424
def populate_state(self, state):
425
state.existing_offering = self.context
427
def get_default_data(self, req):
429
'subject': self.context.subject.short_name,
430
'semester': self.context.semester.year + '/' +
431
self.context.semester.semester,
432
'url': self.context.url,
433
'description': self.context.description,
434
'worksheet_cutoff': self.context.worksheet_cutoff,
435
'show_worksheet_marks': self.context.show_worksheet_marks,
438
def save_object(self, req, data):
440
self.context.subject = data['subject']
441
self.context.semester = data['semester']
442
self.context.description = data['description']
443
self.context.url = unicode(data['url']) if data['url'] else None
444
self.context.worksheet_cutoff = data['worksheet_cutoff']
445
self.context.show_worksheet_marks = data['show_worksheet_marks']
449
class OfferingNew(BaseFormView):
450
"""A form to create an offering."""
451
template = 'templates/offering-new.html'
454
def authorize(self, req):
455
return req.user is not None and req.user.admin
459
return OfferingAdminSchema()
461
def populate(self, req, ctx):
462
super(OfferingNew, self).populate(req, ctx)
463
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
464
ctx['semesters'] = req.store.find(Semester).order_by(
465
Semester.year, Semester.semester)
466
ctx['force_subject'] = None
468
def populate_state(self, state):
469
state.existing_offering = None
471
def get_default_data(self, req):
474
def save_object(self, req, data):
475
new_offering = Offering()
476
new_offering.subject = data['subject']
477
new_offering.semester = data['semester']
478
new_offering.description = data['description']
479
new_offering.url = unicode(data['url']) if data['url'] else None
480
new_offering.worksheet_cutoff = data['worksheet_cutoff']
481
new_offering.show_worksheet_marks = data['show_worksheet_marks']
483
req.store.add(new_offering)
486
class SubjectOfferingNew(OfferingNew):
487
"""A form to create an offering for a given subject."""
488
# Identical to OfferingNew, except it forces the subject to be the subject
490
def populate(self, req, ctx):
491
super(SubjectOfferingNew, self).populate(req, ctx)
492
ctx['force_subject'] = self.context
494
class OfferingCloneWorksheetsSchema(formencode.Schema):
495
subject = formencode.All(
496
SubjectValidator(), formencode.validators.UnicodeString())
497
semester = formencode.All(
498
SemesterValidator(), formencode.validators.UnicodeString())
501
class OfferingCloneWorksheets(BaseFormView):
502
"""A form to clone worksheets from one offering to another."""
503
template = 'templates/offering-clone-worksheets.html'
506
def authorize(self, req):
507
return req.user is not None and req.user.admin
511
return OfferingCloneWorksheetsSchema()
513
def populate(self, req, ctx):
514
super(OfferingCloneWorksheets, self).populate(req, ctx)
515
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
516
ctx['semesters'] = req.store.find(Semester).order_by(
517
Semester.year, Semester.semester)
519
def get_default_data(self, req):
522
def save_object(self, req, data):
523
if self.context.worksheets.count() > 0:
525
"Cannot clone to target with existing worksheets.")
526
offering = req.store.find(
527
Offering, subject=data['subject'], semester=data['semester']).one()
529
raise BadRequest("No such offering.")
530
if offering.worksheets.count() == 0:
531
raise BadRequest("Source offering has no worksheets.")
533
self.context.clone_worksheets(offering)
537
class UserValidator(formencode.FancyValidator):
538
"""A FormEncode validator that turns a username into a user.
540
The state must have a 'store' attribute, which is the Storm store
542
def _to_python(self, value, state):
543
user = User.get_by_login(state.store, value)
547
raise formencode.Invalid('User does not exist', value, state)
550
class NoEnrolmentValidator(formencode.FancyValidator):
551
"""A FormEncode validator that ensures absence of an enrolment.
553
The state must have an 'offering' attribute.
555
def _to_python(self, value, state):
556
if state.offering.get_enrolment(value):
557
raise formencode.Invalid('User already enrolled', value, state)
561
class RoleEnrolmentValidator(formencode.FancyValidator):
562
"""A FormEncode validator that checks permission to enrol users with a
565
The state must have an 'offering' attribute.
567
def _to_python(self, value, state):
568
if (("enrol_" + value) not in
569
state.offering.get_permissions(state.user, state.config)):
570
raise formencode.Invalid('Not allowed to assign users that role',
575
class EnrolSchema(formencode.Schema):
576
user = formencode.All(NoEnrolmentValidator(), UserValidator())
577
role = formencode.All(formencode.validators.OneOf(
578
["lecturer", "tutor", "student"]),
579
RoleEnrolmentValidator(),
580
formencode.validators.UnicodeString())
583
class EnrolmentsView(XHTMLView):
584
"""A page which displays all users enrolled in an offering."""
585
template = 'templates/enrolments.html'
588
breadcrumb_text = 'Enrolments'
590
def populate(self, req, ctx):
592
ctx['offering'] = self.context
593
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
594
ctx['offering_perms'] = self.context.get_permissions(
595
req.user, req.config)
596
ctx['EnrolView'] = EnrolView
597
ctx['EnrolmentEdit'] = EnrolmentEdit
598
ctx['EnrolmentDelete'] = EnrolmentDelete
601
class EnrolView(XHTMLView):
602
"""A form to enrol a user in an offering."""
603
template = 'templates/enrol.html'
607
def filter(self, stream, ctx):
608
return stream | HTMLFormFiller(data=ctx['data'])
610
def populate(self, req, ctx):
611
if req.method == 'POST':
612
data = dict(req.get_fieldstorage())
614
validator = EnrolSchema()
615
req.offering = self.context # XXX: Getting into state.
616
data = validator.to_python(data, state=req)
617
self.context.enrol(data['user'], data['role'])
619
req.throw_redirect(req.uri)
620
except formencode.Invalid, e:
621
errors = e.unpack_errors()
626
ctx['data'] = data or {}
627
ctx['offering'] = self.context
628
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
629
ctx['errors'] = errors
630
# If all of the fields validated, set the global form error.
631
if isinstance(errors, basestring):
632
ctx['error_value'] = errors
635
class EnrolmentEditSchema(formencode.Schema):
636
role = formencode.All(formencode.validators.OneOf(
637
["lecturer", "tutor", "student"]),
638
RoleEnrolmentValidator(),
639
formencode.validators.UnicodeString())
642
class EnrolmentEdit(BaseFormView):
643
"""A form to alter an enrolment's role."""
644
template = 'templates/enrolment-edit.html'
648
def populate_state(self, state):
649
state.offering = self.context.offering
651
def get_default_data(self, req):
652
return {'role': self.context.role}
656
return EnrolmentEditSchema()
658
def save_object(self, req, data):
659
self.context.role = data['role']
661
def get_return_url(self, obj):
662
return self.req.publisher.generate(
663
self.context.offering, EnrolmentsView)
665
def populate(self, req, ctx):
666
super(EnrolmentEdit, self).populate(req, ctx)
667
ctx['offering_perms'] = self.context.offering.get_permissions(
668
req.user, req.config)
671
class EnrolmentDelete(XHTMLView):
672
"""A form to alter an enrolment's role."""
673
template = 'templates/enrolment-delete.html'
677
def populate(self, req, ctx):
678
# If POSTing, delete delete delete.
679
if req.method == 'POST':
680
self.context.delete()
682
req.throw_redirect(req.publisher.generate(
683
self.context.offering, EnrolmentsView))
685
ctx['enrolment'] = self.context
688
class OfferingProjectsView(XHTMLView):
689
"""View the projects for an offering."""
690
template = 'templates/offering_projects.html'
693
breadcrumb_text = 'Projects'
695
def populate(self, req, ctx):
696
self.plugin_styles[Plugin] = ["project.css"]
698
ctx['offering'] = self.context
699
ctx['projectsets'] = []
701
#Open the projectset Fragment, and render it for inclusion
702
#into the ProjectSets page
703
set_fragment = os.path.join(os.path.dirname(__file__),
704
"templates/projectset_fragment.html")
705
project_fragment = os.path.join(os.path.dirname(__file__),
706
"templates/project_fragment.html")
709
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
710
settmpl = self._loader.load(set_fragment)
713
setCtx['projectset'] = projectset
714
setCtx['projects'] = []
715
setCtx['GroupsView'] = GroupsView
716
setCtx['ProjectSetEdit'] = ProjectSetEdit
717
setCtx['ProjectNew'] = ProjectNew
720
projectset.projects.order_by(ivle.database.Project.deadline):
721
projecttmpl = self._loader.load(project_fragment)
722
projectCtx = Context()
723
projectCtx['req'] = req
724
projectCtx['project'] = project
725
projectCtx['ProjectEdit'] = ProjectEdit
726
projectCtx['ProjectDelete'] = ProjectDelete
728
setCtx['projects'].append(
729
projecttmpl.generate(projectCtx))
731
ctx['projectsets'].append(settmpl.generate(setCtx))
734
class ProjectView(XHTMLView):
735
"""View the submissions for a ProjectSet"""
736
template = "templates/project.html"
737
permission = "view_project_submissions"
740
def populate(self, req, ctx):
741
self.plugin_styles[Plugin] = ["project.css"]
744
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
745
ctx['GroupsView'] = GroupsView
746
ctx['EnrolView'] = EnrolView
747
ctx['format_datetime'] = ivle.date.make_date_nice
748
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
749
ctx['project'] = self.context
750
ctx['user'] = req.user
751
ctx['ProjectEdit'] = ProjectEdit
752
ctx['ProjectDelete'] = ProjectDelete
753
ctx['ProjectExport'] = ProjectBashExportView
755
class ProjectBashExportView(TextView):
756
"""Produce a Bash script for exporting projects"""
757
template = "templates/project-export.sh"
758
content_type = "text/x-sh"
759
permission = "view_project_submissions"
761
def populate(self, req, ctx):
763
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
764
ctx['format_datetime'] = ivle.date.make_date_nice
765
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
766
ctx['project'] = self.context
767
ctx['user'] = req.user
768
ctx['now'] = datetime.datetime.now()
769
ctx['format_datetime'] = ivle.date.make_date_nice
770
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
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),
971
(Project, ('+export', 'project-export.sh'),
972
ProjectBashExportView),
975
breadcrumbs = {Subject: SubjectBreadcrumb,
976
Offering: OfferingBreadcrumb,
977
User: UserBreadcrumb,
978
Project: ProjectBreadcrumb,
979
Enrolment: EnrolmentBreadcrumb,
983
('subjects', 'Subjects',
984
'View subject content and complete worksheets',
985
'subjects.png', 'subjects', 5)
988
media = 'subject-media'