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))
313
ctx['exercises_total'] = problems_total
314
ctx['exercises_done'] = problems_done
315
if problems_total > 0:
316
if problems_done >= problems_total:
317
ctx['worksheets_complete_class'] = "complete"
318
elif problems_done > 0:
319
ctx['worksheets_complete_class'] = "semicomplete"
321
ctx['worksheets_complete_class'] = "incomplete"
322
# Calculate the final percentage and mark for the subject
323
(ctx['exercises_pct'], ctx['worksheet_mark'],
324
ctx['worksheet_max_mark']) = (
325
ivle.worksheet.utils.calculate_mark(
326
problems_done, problems_total))
329
class SubjectValidator(formencode.FancyValidator):
330
"""A FormEncode validator that turns a subject name into a subject.
332
The state must have a 'store' attribute, which is the Storm store
335
def _to_python(self, value, state):
336
subject = state.store.find(Subject, short_name=value).one()
340
raise formencode.Invalid('Subject does not exist', value, state)
343
class SemesterValidator(formencode.FancyValidator):
344
"""A FormEncode validator that turns a string into a semester.
346
The string should be of the form 'year/semester', eg. '2009/1'.
348
The state must have a 'store' attribute, which is the Storm store
351
def _to_python(self, value, state):
353
year, semester = value.split('/')
355
year = semester = None
357
semester = state.store.find(
358
Semester, year=year, semester=semester).one()
362
raise formencode.Invalid('Semester does not exist', value, state)
365
class OfferingUniquenessValidator(formencode.FancyValidator):
366
"""A FormEncode validator that checks that an offering is unique.
368
There cannot be more than one offering in the same year and semester.
370
The offering referenced by state.existing_offering is permitted to
371
hold that year and semester tuple. If any other object holds it, the
374
def _to_python(self, value, state):
375
if (state.store.find(
376
Offering, subject=value['subject'],
377
semester=value['semester']).one() not in
378
(None, state.existing_offering)):
379
raise formencode.Invalid(
380
'Offering already exists', value, state)
384
class OfferingSchema(formencode.Schema):
385
description = formencode.validators.UnicodeString(
386
if_missing=None, not_empty=False)
387
url = formencode.validators.URL(if_missing=None, not_empty=False)
388
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
389
show_worksheet_marks = formencode.validators.StringBoolean(
393
class OfferingAdminSchema(OfferingSchema):
394
subject = formencode.All(
395
SubjectValidator(), formencode.validators.UnicodeString())
396
semester = formencode.All(
397
SemesterValidator(), formencode.validators.UnicodeString())
398
chained_validators = [OfferingUniquenessValidator()]
401
class OfferingEdit(BaseFormView):
402
"""A form to edit an offering's details."""
403
template = 'templates/offering-edit.html'
409
if self.req.user.admin:
410
return OfferingAdminSchema()
412
return OfferingSchema()
414
def populate(self, req, ctx):
415
super(OfferingEdit, self).populate(req, ctx)
416
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
417
ctx['semesters'] = req.store.find(Semester).order_by(
418
Semester.year, Semester.semester)
419
ctx['force_subject'] = None
421
def populate_state(self, state):
422
state.existing_offering = self.context
424
def get_default_data(self, req):
426
'subject': self.context.subject.short_name,
427
'semester': self.context.semester.year + '/' +
428
self.context.semester.semester,
429
'url': self.context.url,
430
'description': self.context.description,
431
'worksheet_cutoff': self.context.worksheet_cutoff,
432
'show_worksheet_marks': self.context.show_worksheet_marks,
435
def save_object(self, req, data):
437
self.context.subject = data['subject']
438
self.context.semester = data['semester']
439
self.context.description = data['description']
440
self.context.url = unicode(data['url']) if data['url'] else None
441
self.context.worksheet_cutoff = data['worksheet_cutoff']
442
self.context.show_worksheet_marks = data['show_worksheet_marks']
446
class OfferingNew(BaseFormView):
447
"""A form to create an offering."""
448
template = 'templates/offering-new.html'
451
def authorize(self, req):
452
return req.user is not None and req.user.admin
456
return OfferingAdminSchema()
458
def populate(self, req, ctx):
459
super(OfferingNew, self).populate(req, ctx)
460
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
461
ctx['semesters'] = req.store.find(Semester).order_by(
462
Semester.year, Semester.semester)
463
ctx['force_subject'] = None
465
def populate_state(self, state):
466
state.existing_offering = None
468
def get_default_data(self, req):
471
def save_object(self, req, data):
472
new_offering = Offering()
473
new_offering.subject = data['subject']
474
new_offering.semester = data['semester']
475
new_offering.description = data['description']
476
new_offering.url = unicode(data['url']) if data['url'] else None
477
new_offering.worksheet_cutoff = data['worksheet_cutoff']
478
new_offering.show_worksheet_marks = data['show_worksheet_marks']
480
req.store.add(new_offering)
483
class SubjectOfferingNew(OfferingNew):
484
"""A form to create an offering for a given subject."""
485
# Identical to OfferingNew, except it forces the subject to be the subject
487
def populate(self, req, ctx):
488
super(SubjectOfferingNew, self).populate(req, ctx)
489
ctx['force_subject'] = self.context
491
class OfferingCloneWorksheetsSchema(formencode.Schema):
492
subject = formencode.All(
493
SubjectValidator(), formencode.validators.UnicodeString())
494
semester = formencode.All(
495
SemesterValidator(), formencode.validators.UnicodeString())
498
class OfferingCloneWorksheets(BaseFormView):
499
"""A form to clone worksheets from one offering to another."""
500
template = 'templates/offering-clone-worksheets.html'
503
def authorize(self, req):
504
return req.user is not None and req.user.admin
508
return OfferingCloneWorksheetsSchema()
510
def populate(self, req, ctx):
511
super(OfferingCloneWorksheets, self).populate(req, ctx)
512
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
513
ctx['semesters'] = req.store.find(Semester).order_by(
514
Semester.year, Semester.semester)
516
def get_default_data(self, req):
519
def save_object(self, req, data):
520
if self.context.worksheets.count() > 0:
522
"Cannot clone to target with existing worksheets.")
523
offering = req.store.find(
524
Offering, subject=data['subject'], semester=data['semester']).one()
526
raise BadRequest("No such offering.")
527
if offering.worksheets.count() == 0:
528
raise BadRequest("Source offering has no worksheets.")
530
self.context.clone_worksheets(offering)
534
class UserValidator(formencode.FancyValidator):
535
"""A FormEncode validator that turns a username into a user.
537
The state must have a 'store' attribute, which is the Storm store
539
def _to_python(self, value, state):
540
user = User.get_by_login(state.store, value)
544
raise formencode.Invalid('User does not exist', value, state)
547
class NoEnrolmentValidator(formencode.FancyValidator):
548
"""A FormEncode validator that ensures absence of an enrolment.
550
The state must have an 'offering' attribute.
552
def _to_python(self, value, state):
553
if state.offering.get_enrolment(value):
554
raise formencode.Invalid('User already enrolled', value, state)
558
class RoleEnrolmentValidator(formencode.FancyValidator):
559
"""A FormEncode validator that checks permission to enrol users with a
562
The state must have an 'offering' attribute.
564
def _to_python(self, value, state):
565
if (("enrol_" + value) not in
566
state.offering.get_permissions(state.user, state.config)):
567
raise formencode.Invalid('Not allowed to assign users that role',
572
class EnrolSchema(formencode.Schema):
573
user = formencode.All(NoEnrolmentValidator(), UserValidator())
574
role = formencode.All(formencode.validators.OneOf(
575
["lecturer", "tutor", "student"]),
576
RoleEnrolmentValidator(),
577
formencode.validators.UnicodeString())
580
class EnrolmentsView(XHTMLView):
581
"""A page which displays all users enrolled in an offering."""
582
template = 'templates/enrolments.html'
585
breadcrumb_text = 'Enrolments'
587
def populate(self, req, ctx):
589
ctx['offering'] = self.context
590
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
591
ctx['offering_perms'] = self.context.get_permissions(
592
req.user, req.config)
593
ctx['EnrolView'] = EnrolView
594
ctx['EnrolmentEdit'] = EnrolmentEdit
595
ctx['EnrolmentDelete'] = EnrolmentDelete
598
class EnrolView(XHTMLView):
599
"""A form to enrol a user in an offering."""
600
template = 'templates/enrol.html'
604
def filter(self, stream, ctx):
605
return stream | HTMLFormFiller(data=ctx['data'])
607
def populate(self, req, ctx):
608
if req.method == 'POST':
609
data = dict(req.get_fieldstorage())
611
validator = EnrolSchema()
612
req.offering = self.context # XXX: Getting into state.
613
data = validator.to_python(data, state=req)
614
self.context.enrol(data['user'], data['role'])
616
req.throw_redirect(req.uri)
617
except formencode.Invalid, e:
618
errors = e.unpack_errors()
623
ctx['data'] = data or {}
624
ctx['offering'] = self.context
625
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
626
ctx['errors'] = errors
627
# If all of the fields validated, set the global form error.
628
if isinstance(errors, basestring):
629
ctx['error_value'] = errors
632
class EnrolmentEditSchema(formencode.Schema):
633
role = formencode.All(formencode.validators.OneOf(
634
["lecturer", "tutor", "student"]),
635
RoleEnrolmentValidator(),
636
formencode.validators.UnicodeString())
639
class EnrolmentEdit(BaseFormView):
640
"""A form to alter an enrolment's role."""
641
template = 'templates/enrolment-edit.html'
645
def populate_state(self, state):
646
state.offering = self.context.offering
648
def get_default_data(self, req):
649
return {'role': self.context.role}
653
return EnrolmentEditSchema()
655
def save_object(self, req, data):
656
self.context.role = data['role']
658
def get_return_url(self, obj):
659
return self.req.publisher.generate(
660
self.context.offering, EnrolmentsView)
662
def populate(self, req, ctx):
663
super(EnrolmentEdit, self).populate(req, ctx)
664
ctx['offering_perms'] = self.context.offering.get_permissions(
665
req.user, req.config)
668
class EnrolmentDelete(XHTMLView):
669
"""A form to alter an enrolment's role."""
670
template = 'templates/enrolment-delete.html'
674
def populate(self, req, ctx):
675
# If POSTing, delete delete delete.
676
if req.method == 'POST':
677
self.context.delete()
679
req.throw_redirect(req.publisher.generate(
680
self.context.offering, EnrolmentsView))
682
ctx['enrolment'] = self.context
685
class OfferingProjectsView(XHTMLView):
686
"""View the projects for an offering."""
687
template = 'templates/offering_projects.html'
690
breadcrumb_text = 'Projects'
692
def populate(self, req, ctx):
693
self.plugin_styles[Plugin] = ["project.css"]
695
ctx['offering'] = self.context
696
ctx['projectsets'] = []
698
#Open the projectset Fragment, and render it for inclusion
699
#into the ProjectSets page
700
set_fragment = os.path.join(os.path.dirname(__file__),
701
"templates/projectset_fragment.html")
702
project_fragment = os.path.join(os.path.dirname(__file__),
703
"templates/project_fragment.html")
706
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
707
settmpl = self._loader.load(set_fragment)
710
setCtx['projectset'] = projectset
711
setCtx['projects'] = []
712
setCtx['GroupsView'] = GroupsView
713
setCtx['ProjectSetEdit'] = ProjectSetEdit
714
setCtx['ProjectNew'] = ProjectNew
717
projectset.projects.order_by(ivle.database.Project.deadline):
718
projecttmpl = self._loader.load(project_fragment)
719
projectCtx = Context()
720
projectCtx['req'] = req
721
projectCtx['project'] = project
722
projectCtx['ProjectEdit'] = ProjectEdit
723
projectCtx['ProjectDelete'] = ProjectDelete
725
setCtx['projects'].append(
726
projecttmpl.generate(projectCtx))
728
ctx['projectsets'].append(settmpl.generate(setCtx))
731
class ProjectView(XHTMLView):
732
"""View the submissions for a ProjectSet"""
733
template = "templates/project.html"
734
permission = "view_project_submissions"
737
def build_subversion_url(self, svnroot, submission):
738
princ = submission.assessed.principal
740
if isinstance(princ, User):
741
path = 'users/%s' % princ.login
743
path = 'groups/%s_%s_%s_%s' % (
744
princ.project_set.offering.subject.short_name,
745
princ.project_set.offering.semester.year,
746
princ.project_set.offering.semester.semester,
749
return urlparse.urljoin(
751
os.path.join(path, submission.path[1:] if
752
submission.path.startswith(os.sep) else
755
def populate(self, req, ctx):
756
self.plugin_styles[Plugin] = ["project.css"]
759
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
760
ctx['GroupsView'] = GroupsView
761
ctx['EnrolView'] = EnrolView
762
ctx['format_datetime'] = ivle.date.make_date_nice
763
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
764
ctx['build_subversion_url'] = self.build_subversion_url
765
ctx['svn_addr'] = req.config['urls']['svn_addr']
766
ctx['project'] = self.context
767
ctx['user'] = req.user
768
ctx['ProjectEdit'] = ProjectEdit
769
ctx['ProjectDelete'] = ProjectDelete
771
class ProjectUniquenessValidator(formencode.FancyValidator):
772
"""A FormEncode validator that checks that a project short_name is unique
775
The project referenced by state.existing_project is permitted to
776
hold that short_name. If any other project holds it, the input is rejected.
778
def _to_python(self, value, state):
779
if (state.store.find(
781
Project.short_name == unicode(value),
782
Project.project_set_id == ProjectSet.id,
783
ProjectSet.offering == state.offering).one() not in
784
(None, state.existing_project)):
785
raise formencode.Invalid(
786
"A project with that URL name already exists in this offering."
790
class ProjectSchema(formencode.Schema):
791
name = formencode.validators.UnicodeString(not_empty=True)
792
short_name = formencode.All(
793
URLNameValidator(not_empty=True),
794
ProjectUniquenessValidator())
795
deadline = DateTimeValidator(not_empty=True)
796
url = formencode.validators.URL(if_missing=None, not_empty=False)
797
synopsis = formencode.validators.UnicodeString(not_empty=True)
799
class ProjectEdit(BaseFormView):
800
"""A form to edit a project."""
801
template = 'templates/project-edit.html'
807
return ProjectSchema()
809
def populate(self, req, ctx):
810
super(ProjectEdit, self).populate(req, ctx)
811
ctx['projectset'] = self.context.project_set
813
def populate_state(self, state):
814
state.offering = self.context.project_set.offering
815
state.existing_project = self.context
817
def get_default_data(self, req):
819
'name': self.context.name,
820
'short_name': self.context.short_name,
821
'deadline': self.context.deadline,
822
'url': self.context.url,
823
'synopsis': self.context.synopsis,
826
def save_object(self, req, data):
827
self.context.name = data['name']
828
self.context.short_name = data['short_name']
829
self.context.deadline = data['deadline']
830
self.context.url = unicode(data['url']) if data['url'] else None
831
self.context.synopsis = data['synopsis']
834
class ProjectNew(BaseFormView):
835
"""A form to create a new project."""
836
template = 'templates/project-new.html'
842
return ProjectSchema()
844
def populate(self, req, ctx):
845
super(ProjectNew, self).populate(req, ctx)
846
ctx['projectset'] = self.context
848
def populate_state(self, state):
849
state.offering = self.context.offering
850
state.existing_project = None
852
def get_default_data(self, req):
855
def save_object(self, req, data):
856
new_project = Project()
857
new_project.project_set = self.context
858
new_project.name = data['name']
859
new_project.short_name = data['short_name']
860
new_project.deadline = data['deadline']
861
new_project.url = unicode(data['url']) if data['url'] else None
862
new_project.synopsis = data['synopsis']
863
req.store.add(new_project)
866
class ProjectDelete(XHTMLView):
867
"""A form to delete a project."""
868
template = 'templates/project-delete.html'
872
def populate(self, req, ctx):
873
# If post, delete the project, or display a message explaining that
874
# the project cannot be deleted
875
if self.context.can_delete:
876
if req.method == 'POST':
877
self.context.delete()
878
self.template = 'templates/project-deleted.html'
881
self.template = 'templates/project-undeletable.html'
883
# If get and can delete, display a delete confirmation page
885
# Variables for the template
887
ctx['project'] = self.context
888
ctx['OfferingProjectsView'] = OfferingProjectsView
890
class ProjectSetSchema(formencode.Schema):
891
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
893
class ProjectSetEdit(BaseFormView):
894
"""A form to edit a project set."""
895
template = 'templates/projectset-edit.html'
901
return ProjectSetSchema()
903
def populate(self, req, ctx):
904
super(ProjectSetEdit, self).populate(req, ctx)
906
def get_default_data(self, req):
908
'group_size': self.context.max_students_per_group,
911
def save_object(self, req, data):
912
self.context.max_students_per_group = data['group_size']
915
class ProjectSetNew(BaseFormView):
916
"""A form to create a new project set."""
917
template = 'templates/projectset-new.html'
920
breadcrumb_text = "Projects"
924
return ProjectSetSchema()
926
def populate(self, req, ctx):
927
super(ProjectSetNew, self).populate(req, ctx)
929
def get_default_data(self, req):
932
def save_object(self, req, data):
933
new_set = ProjectSet()
934
new_set.offering = self.context
935
new_set.max_students_per_group = data['group_size']
936
req.store.add(new_set)
939
class Plugin(ViewPlugin, MediaPlugin):
940
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
941
offering_to_project, offering_to_projectset,
942
offering_to_enrolment)
944
subject_url, semester_url, offering_url, projectset_url, project_url,
947
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
948
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
949
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
950
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
951
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
952
(Subject, '+index', SubjectView),
953
(Subject, '+edit', SubjectEdit),
954
(Subject, '+new-offering', SubjectOfferingNew),
955
(Semester, '+edit', SemesterEdit),
956
(Offering, '+index', OfferingView),
957
(Offering, '+edit', OfferingEdit),
958
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
959
(Offering, ('+enrolments', '+index'), EnrolmentsView),
960
(Offering, ('+enrolments', '+new'), EnrolView),
961
(Enrolment, '+edit', EnrolmentEdit),
962
(Enrolment, '+delete', EnrolmentDelete),
963
(Offering, ('+projects', '+index'), OfferingProjectsView),
964
(Offering, ('+projects', '+new-set'), ProjectSetNew),
965
(ProjectSet, '+edit', ProjectSetEdit),
966
(ProjectSet, '+new', ProjectNew),
967
(Project, '+index', ProjectView),
968
(Project, '+edit', ProjectEdit),
969
(Project, '+delete', ProjectDelete),
972
breadcrumbs = {Subject: SubjectBreadcrumb,
973
Offering: OfferingBreadcrumb,
974
User: UserBreadcrumb,
975
Project: ProjectBreadcrumb,
976
Enrolment: EnrolmentBreadcrumb,
980
('subjects', 'Subjects',
981
'View subject content and complete worksheets',
982
'subjects.png', 'subjects', 5)
985
media = 'subject-media'