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(
79
Desc(Semester.year), Desc(Semester.display_name)):
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.display_name)
110
class SubjectUniquenessValidator(formencode.FancyValidator):
111
"""A FormEncode validator that checks that a subject attribute is unique.
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
:param attribute: the name of the attribute to check.
117
:param display: a string to identify the field in case of error.
120
def __init__(self, attribute, display):
121
self.attribute = attribute
122
self.display = display
124
def _to_python(self, value, state):
125
if (state.store.find(Subject, **{self.attribute: value}).one() not in
126
(None, state.existing_subject)):
127
raise formencode.Invalid(
128
'%s already taken' % self.display, value, state)
132
class SubjectSchema(formencode.Schema):
133
short_name = formencode.All(
134
SubjectUniquenessValidator('short_name', 'URL name'),
135
URLNameValidator(not_empty=True))
136
name = formencode.validators.UnicodeString(not_empty=True)
137
code = formencode.All(
138
SubjectUniquenessValidator('code', 'Subject code'),
139
formencode.validators.UnicodeString(not_empty=True))
142
class SubjectFormView(BaseFormView):
143
"""An abstract form to add or edit a subject."""
146
def authorize(self, req):
147
return req.user is not None and req.user.admin
149
def populate_state(self, state):
150
state.existing_subject = None
154
return SubjectSchema()
157
class SubjectNew(SubjectFormView):
158
"""A form to create a subject."""
159
template = 'templates/subject-new.html'
161
def get_default_data(self, req):
164
def save_object(self, req, data):
165
new_subject = Subject()
166
new_subject.short_name = data['short_name']
167
new_subject.name = data['name']
168
new_subject.code = data['code']
170
req.store.add(new_subject)
174
class SubjectEdit(SubjectFormView):
175
"""A form to edit a subject."""
176
template = 'templates/subject-edit.html'
178
def populate_state(self, state):
179
state.existing_subject = self.context
181
def get_default_data(self, req):
183
'short_name': self.context.short_name,
184
'name': self.context.name,
185
'code': self.context.code,
188
def save_object(self, req, data):
189
self.context.short_name = data['short_name']
190
self.context.name = data['name']
191
self.context.code = data['code']
196
class SemesterUniquenessValidator(formencode.FancyValidator):
197
"""A FormEncode validator that checks that a semester is unique.
199
There cannot be more than one semester for the same year and semester.
201
def _to_python(self, value, state):
202
if (state.store.find(
203
Semester, year=value['year'], url_name=value['url_name']
204
).one() not in (None, state.existing_semester)):
205
raise formencode.Invalid(
206
'Semester already exists', value, state)
210
class SemesterSchema(formencode.Schema):
211
year = URLNameValidator()
212
code = formencode.validators.UnicodeString()
213
url_name = URLNameValidator()
214
display_name = formencode.validators.UnicodeString()
215
state = formencode.All(
216
formencode.validators.OneOf(["past", "current", "future"]),
217
formencode.validators.UnicodeString())
218
chained_validators = [SemesterUniquenessValidator()]
221
class SemesterFormView(BaseFormView):
224
def authorize(self, req):
225
return req.user is not None and req.user.admin
229
return SemesterSchema()
231
def get_return_url(self, obj):
232
return '/subjects/+manage'
235
class SemesterNew(SemesterFormView):
236
"""A form to create a semester."""
237
template = 'templates/semester-new.html'
240
def populate_state(self, state):
241
state.existing_semester = None
243
def get_default_data(self, req):
246
def save_object(self, req, data):
247
new_semester = Semester()
248
new_semester.year = data['year']
249
new_semester.code = data['code']
250
new_semester.url_name = data['url_name']
251
new_semester.display_name = data['display_name']
252
new_semester.state = data['state']
254
req.store.add(new_semester)
258
class SemesterEdit(SemesterFormView):
259
"""A form to edit a semester."""
260
template = 'templates/semester-edit.html'
262
def populate_state(self, state):
263
state.existing_semester = self.context
265
def get_default_data(self, req):
267
'year': self.context.year,
268
'code': self.context.code,
269
'url_name': self.context.url_name,
270
'display_name': self.context.display_name,
271
'state': self.context.state,
274
def save_object(self, req, data):
275
self.context.year = data['year']
276
self.context.code = data['code']
277
self.context.url_name = data['url_name']
278
self.context.display_name = data['display_name']
279
self.context.state = data['state']
283
class SubjectView(XHTMLView):
284
'''The view of the list of offerings in a given subject.'''
285
template = 'templates/subject.html'
288
def authorize(self, req):
289
return req.user is not None
291
def populate(self, req, ctx):
292
ctx['context'] = self.context
294
ctx['user'] = req.user
295
ctx['offerings'] = list(self.context.offerings)
296
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
297
ctx['SubjectEdit'] = SubjectEdit
298
ctx['SubjectOfferingNew'] = SubjectOfferingNew
301
class OfferingView(XHTMLView):
302
"""The home page of an offering."""
303
template = 'templates/offering.html'
307
def populate(self, req, ctx):
308
# Need the worksheet result styles.
309
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
310
ctx['context'] = self.context
312
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
313
ctx['format_submission_principal'] = util.format_submission_principal
314
ctx['format_datetime'] = ivle.date.make_date_nice
315
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
316
ctx['OfferingEdit'] = OfferingEdit
317
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
318
ctx['GroupsView'] = GroupsView
319
ctx['EnrolmentsView'] = EnrolmentsView
320
ctx['Project'] = ivle.database.Project
322
# As we go, calculate the total score for this subject
323
# (Assessable worksheets only, mandatory problems only)
325
ctx['worksheets'], problems_total, problems_done = (
326
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
327
req.config, req.store, req.user, self.context,
328
as_of=self.context.worksheet_cutoff))
330
ctx['exercises_total'] = problems_total
331
ctx['exercises_done'] = problems_done
332
if problems_total > 0:
333
if problems_done >= problems_total:
334
ctx['worksheets_complete_class'] = "complete"
335
elif problems_done > 0:
336
ctx['worksheets_complete_class'] = "semicomplete"
338
ctx['worksheets_complete_class'] = "incomplete"
339
# Calculate the final percentage and mark for the subject
340
(ctx['exercises_pct'], ctx['worksheet_mark'],
341
ctx['worksheet_max_mark']) = (
342
ivle.worksheet.utils.calculate_mark(
343
problems_done, problems_total))
346
class SubjectValidator(formencode.FancyValidator):
347
"""A FormEncode validator that turns a subject name into a subject.
349
The state must have a 'store' attribute, which is the Storm store
352
def _to_python(self, value, state):
353
subject = state.store.find(Subject, short_name=value).one()
357
raise formencode.Invalid('Subject does not exist', value, state)
360
class SemesterValidator(formencode.FancyValidator):
361
"""A FormEncode validator that turns a string into a semester.
363
The string should be of the form 'year/semester', eg. '2009/1'.
365
The state must have a 'store' attribute, which is the Storm store
368
def _to_python(self, value, state):
370
year, semester = value.split('/')
372
year = semester = None
374
semester = state.store.find(
375
Semester, year=year, url_name=semester).one()
379
raise formencode.Invalid('Semester does not exist', value, state)
382
class OfferingUniquenessValidator(formencode.FancyValidator):
383
"""A FormEncode validator that checks that an offering is unique.
385
There cannot be more than one offering in the same year and semester.
387
The offering referenced by state.existing_offering is permitted to
388
hold that year and semester tuple. If any other object holds it, the
391
def _to_python(self, value, state):
392
if (state.store.find(
393
Offering, subject=value['subject'],
394
semester=value['semester']).one() not in
395
(None, state.existing_offering)):
396
raise formencode.Invalid(
397
'Offering already exists', value, state)
401
class OfferingSchema(formencode.Schema):
402
description = formencode.validators.UnicodeString(
403
if_missing=None, not_empty=False)
404
url = formencode.validators.URL(if_missing=None, not_empty=False)
405
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
406
show_worksheet_marks = formencode.validators.StringBoolean(
410
class OfferingAdminSchema(OfferingSchema):
411
subject = formencode.All(
412
SubjectValidator(), formencode.validators.UnicodeString())
413
semester = formencode.All(
414
SemesterValidator(), formencode.validators.UnicodeString())
415
chained_validators = [OfferingUniquenessValidator()]
418
class OfferingEdit(BaseFormView):
419
"""A form to edit an offering's details."""
420
template = 'templates/offering-edit.html'
426
if self.req.user.admin:
427
return OfferingAdminSchema()
429
return OfferingSchema()
431
def populate(self, req, ctx):
432
super(OfferingEdit, self).populate(req, ctx)
433
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
434
ctx['semesters'] = req.store.find(Semester).order_by(
435
Semester.year, Semester.display_name)
436
ctx['force_subject'] = None
438
def populate_state(self, state):
439
state.existing_offering = self.context
441
def get_default_data(self, req):
443
'subject': self.context.subject.short_name,
444
'semester': self.context.semester.year + '/' +
445
self.context.semester.url_name,
446
'url': self.context.url,
447
'description': self.context.description,
448
'worksheet_cutoff': self.context.worksheet_cutoff,
449
'show_worksheet_marks': self.context.show_worksheet_marks,
452
def save_object(self, req, data):
454
self.context.subject = data['subject']
455
self.context.semester = data['semester']
456
self.context.description = data['description']
457
self.context.url = unicode(data['url']) if data['url'] else None
458
self.context.worksheet_cutoff = data['worksheet_cutoff']
459
self.context.show_worksheet_marks = data['show_worksheet_marks']
463
class OfferingNew(BaseFormView):
464
"""A form to create an offering."""
465
template = 'templates/offering-new.html'
468
def authorize(self, req):
469
return req.user is not None and req.user.admin
473
return OfferingAdminSchema()
475
def populate(self, req, ctx):
476
super(OfferingNew, self).populate(req, ctx)
477
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
478
ctx['semesters'] = req.store.find(Semester).order_by(
479
Semester.year, Semester.display_name)
480
ctx['force_subject'] = None
482
def populate_state(self, state):
483
state.existing_offering = None
485
def get_default_data(self, req):
488
def save_object(self, req, data):
489
new_offering = Offering()
490
new_offering.subject = data['subject']
491
new_offering.semester = data['semester']
492
new_offering.description = data['description']
493
new_offering.url = unicode(data['url']) if data['url'] else None
494
new_offering.worksheet_cutoff = data['worksheet_cutoff']
495
new_offering.show_worksheet_marks = data['show_worksheet_marks']
497
req.store.add(new_offering)
500
class SubjectOfferingNew(OfferingNew):
501
"""A form to create an offering for a given subject."""
502
# Identical to OfferingNew, except it forces the subject to be the subject
504
def populate(self, req, ctx):
505
super(SubjectOfferingNew, self).populate(req, ctx)
506
ctx['force_subject'] = self.context
508
class OfferingCloneWorksheetsSchema(formencode.Schema):
509
subject = formencode.All(
510
SubjectValidator(), formencode.validators.UnicodeString())
511
semester = formencode.All(
512
SemesterValidator(), formencode.validators.UnicodeString())
515
class OfferingCloneWorksheets(BaseFormView):
516
"""A form to clone worksheets from one offering to another."""
517
template = 'templates/offering-clone-worksheets.html'
520
def authorize(self, req):
521
return req.user is not None and req.user.admin
525
return OfferingCloneWorksheetsSchema()
527
def populate(self, req, ctx):
528
super(OfferingCloneWorksheets, self).populate(req, ctx)
529
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
530
ctx['semesters'] = req.store.find(Semester).order_by(
531
Semester.year, Semester.display_name)
533
def get_default_data(self, req):
536
def save_object(self, req, data):
537
if self.context.worksheets.count() > 0:
539
"Cannot clone to target with existing worksheets.")
540
offering = req.store.find(
541
Offering, subject=data['subject'], semester=data['semester']).one()
543
raise BadRequest("No such offering.")
544
if offering.worksheets.count() == 0:
545
raise BadRequest("Source offering has no worksheets.")
547
self.context.clone_worksheets(offering)
551
class UserValidator(formencode.FancyValidator):
552
"""A FormEncode validator that turns a username into a user.
554
The state must have a 'store' attribute, which is the Storm store
556
def _to_python(self, value, state):
557
user = User.get_by_login(state.store, value)
561
raise formencode.Invalid('User does not exist', value, state)
564
class NoEnrolmentValidator(formencode.FancyValidator):
565
"""A FormEncode validator that ensures absence of an enrolment.
567
The state must have an 'offering' attribute.
569
def _to_python(self, value, state):
570
if state.offering.get_enrolment(value):
571
raise formencode.Invalid('User already enrolled', value, state)
575
class RoleEnrolmentValidator(formencode.FancyValidator):
576
"""A FormEncode validator that checks permission to enrol users with a
579
The state must have an 'offering' attribute.
581
def _to_python(self, value, state):
582
if (("enrol_" + value) not in
583
state.offering.get_permissions(state.user, state.config)):
584
raise formencode.Invalid('Not allowed to assign users that role',
589
class EnrolSchema(formencode.Schema):
590
user = formencode.All(NoEnrolmentValidator(), UserValidator())
591
role = formencode.All(formencode.validators.OneOf(
592
["lecturer", "tutor", "student"]),
593
RoleEnrolmentValidator(),
594
formencode.validators.UnicodeString())
597
class EnrolmentsView(XHTMLView):
598
"""A page which displays all users enrolled in an offering."""
599
template = 'templates/enrolments.html'
602
breadcrumb_text = 'Enrolments'
604
def populate(self, req, ctx):
606
ctx['offering'] = self.context
607
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
608
ctx['offering_perms'] = self.context.get_permissions(
609
req.user, req.config)
610
ctx['EnrolView'] = EnrolView
611
ctx['EnrolmentEdit'] = EnrolmentEdit
612
ctx['EnrolmentDelete'] = EnrolmentDelete
615
class EnrolView(XHTMLView):
616
"""A form to enrol a user in an offering."""
617
template = 'templates/enrol.html'
621
def filter(self, stream, ctx):
622
return stream | HTMLFormFiller(data=ctx['data'])
624
def populate(self, req, ctx):
625
if req.method == 'POST':
626
data = dict(req.get_fieldstorage())
628
validator = EnrolSchema()
629
req.offering = self.context # XXX: Getting into state.
630
data = validator.to_python(data, state=req)
631
self.context.enrol(data['user'], data['role'])
633
req.throw_redirect(req.uri)
634
except formencode.Invalid, e:
635
errors = e.unpack_errors()
640
ctx['data'] = data or {}
641
ctx['offering'] = self.context
642
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
643
ctx['errors'] = errors
644
# If all of the fields validated, set the global form error.
645
if isinstance(errors, basestring):
646
ctx['error_value'] = errors
649
class EnrolmentEditSchema(formencode.Schema):
650
role = formencode.All(formencode.validators.OneOf(
651
["lecturer", "tutor", "student"]),
652
RoleEnrolmentValidator(),
653
formencode.validators.UnicodeString())
656
class EnrolmentEdit(BaseFormView):
657
"""A form to alter an enrolment's role."""
658
template = 'templates/enrolment-edit.html'
662
def populate_state(self, state):
663
state.offering = self.context.offering
665
def get_default_data(self, req):
666
return {'role': self.context.role}
670
return EnrolmentEditSchema()
672
def save_object(self, req, data):
673
self.context.role = data['role']
675
def get_return_url(self, obj):
676
return self.req.publisher.generate(
677
self.context.offering, EnrolmentsView)
679
def populate(self, req, ctx):
680
super(EnrolmentEdit, self).populate(req, ctx)
681
ctx['offering_perms'] = self.context.offering.get_permissions(
682
req.user, req.config)
685
class EnrolmentDelete(XHTMLView):
686
"""A form to alter an enrolment's role."""
687
template = 'templates/enrolment-delete.html'
691
def populate(self, req, ctx):
692
# If POSTing, delete delete delete.
693
if req.method == 'POST':
694
self.context.delete()
696
req.throw_redirect(req.publisher.generate(
697
self.context.offering, EnrolmentsView))
699
ctx['enrolment'] = self.context
702
class OfferingProjectsView(XHTMLView):
703
"""View the projects for an offering."""
704
template = 'templates/offering_projects.html'
707
breadcrumb_text = 'Projects'
709
def populate(self, req, ctx):
710
self.plugin_styles[Plugin] = ["project.css"]
712
ctx['offering'] = self.context
713
ctx['projectsets'] = []
715
#Open the projectset Fragment, and render it for inclusion
716
#into the ProjectSets page
717
set_fragment = os.path.join(os.path.dirname(__file__),
718
"templates/projectset_fragment.html")
719
project_fragment = os.path.join(os.path.dirname(__file__),
720
"templates/project_fragment.html")
723
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
724
settmpl = self._loader.load(set_fragment)
727
setCtx['projectset'] = projectset
728
setCtx['projects'] = []
729
setCtx['GroupsView'] = GroupsView
730
setCtx['ProjectSetEdit'] = ProjectSetEdit
731
setCtx['ProjectNew'] = ProjectNew
734
projectset.projects.order_by(ivle.database.Project.deadline):
735
projecttmpl = self._loader.load(project_fragment)
736
projectCtx = Context()
737
projectCtx['req'] = req
738
projectCtx['project'] = project
739
projectCtx['ProjectEdit'] = ProjectEdit
740
projectCtx['ProjectDelete'] = ProjectDelete
742
setCtx['projects'].append(
743
projecttmpl.generate(projectCtx))
745
ctx['projectsets'].append(settmpl.generate(setCtx))
748
class ProjectView(XHTMLView):
749
"""View the submissions for a ProjectSet"""
750
template = "templates/project.html"
751
permission = "view_project_submissions"
754
def populate(self, req, ctx):
755
self.plugin_styles[Plugin] = ["project.css"]
758
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
759
ctx['GroupsView'] = GroupsView
760
ctx['EnrolView'] = EnrolView
761
ctx['format_datetime'] = ivle.date.make_date_nice
762
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
763
ctx['project'] = self.context
764
ctx['user'] = req.user
765
ctx['ProjectEdit'] = ProjectEdit
766
ctx['ProjectDelete'] = ProjectDelete
767
ctx['ProjectExport'] = ProjectBashExportView
769
class ProjectBashExportView(TextView):
770
"""Produce a Bash script for exporting projects"""
771
template = "templates/project-export.sh"
772
content_type = "text/x-sh"
773
permission = "view_project_submissions"
775
def populate(self, req, ctx):
777
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
778
ctx['format_datetime'] = ivle.date.make_date_nice
779
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
780
ctx['project'] = self.context
781
ctx['user'] = req.user
782
ctx['now'] = datetime.datetime.now()
783
ctx['format_datetime'] = ivle.date.make_date_nice
784
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
786
class ProjectUniquenessValidator(formencode.FancyValidator):
787
"""A FormEncode validator that checks that a project short_name is unique
790
The project referenced by state.existing_project is permitted to
791
hold that short_name. If any other project holds it, the input is rejected.
793
def _to_python(self, value, state):
794
if (state.store.find(
796
Project.short_name == unicode(value),
797
Project.project_set_id == ProjectSet.id,
798
ProjectSet.offering == state.offering).one() not in
799
(None, state.existing_project)):
800
raise formencode.Invalid(
801
"A project with that URL name already exists in this offering."
805
class ProjectSchema(formencode.Schema):
806
name = formencode.validators.UnicodeString(not_empty=True)
807
short_name = formencode.All(
808
URLNameValidator(not_empty=True),
809
ProjectUniquenessValidator())
810
deadline = DateTimeValidator(not_empty=True)
811
url = formencode.validators.URL(if_missing=None, not_empty=False)
812
synopsis = formencode.validators.UnicodeString(not_empty=True)
814
class ProjectEdit(BaseFormView):
815
"""A form to edit a project."""
816
template = 'templates/project-edit.html'
822
return ProjectSchema()
824
def populate(self, req, ctx):
825
super(ProjectEdit, self).populate(req, ctx)
826
ctx['projectset'] = self.context.project_set
828
def populate_state(self, state):
829
state.offering = self.context.project_set.offering
830
state.existing_project = self.context
832
def get_default_data(self, req):
834
'name': self.context.name,
835
'short_name': self.context.short_name,
836
'deadline': self.context.deadline,
837
'url': self.context.url,
838
'synopsis': self.context.synopsis,
841
def save_object(self, req, data):
842
self.context.name = data['name']
843
self.context.short_name = data['short_name']
844
self.context.deadline = data['deadline']
845
self.context.url = unicode(data['url']) if data['url'] else None
846
self.context.synopsis = data['synopsis']
849
class ProjectNew(BaseFormView):
850
"""A form to create a new project."""
851
template = 'templates/project-new.html'
857
return ProjectSchema()
859
def populate(self, req, ctx):
860
super(ProjectNew, self).populate(req, ctx)
861
ctx['projectset'] = self.context
863
def populate_state(self, state):
864
state.offering = self.context.offering
865
state.existing_project = None
867
def get_default_data(self, req):
870
def save_object(self, req, data):
871
new_project = Project()
872
new_project.project_set = self.context
873
new_project.name = data['name']
874
new_project.short_name = data['short_name']
875
new_project.deadline = data['deadline']
876
new_project.url = unicode(data['url']) if data['url'] else None
877
new_project.synopsis = data['synopsis']
878
req.store.add(new_project)
881
class ProjectDelete(XHTMLView):
882
"""A form to delete a project."""
883
template = 'templates/project-delete.html'
887
def populate(self, req, ctx):
888
# If post, delete the project, or display a message explaining that
889
# the project cannot be deleted
890
if self.context.can_delete:
891
if req.method == 'POST':
892
self.context.delete()
893
self.template = 'templates/project-deleted.html'
896
self.template = 'templates/project-undeletable.html'
898
# If get and can delete, display a delete confirmation page
900
# Variables for the template
902
ctx['project'] = self.context
903
ctx['OfferingProjectsView'] = OfferingProjectsView
905
class ProjectSetSchema(formencode.Schema):
906
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
908
class ProjectSetEdit(BaseFormView):
909
"""A form to edit a project set."""
910
template = 'templates/projectset-edit.html'
916
return ProjectSetSchema()
918
def populate(self, req, ctx):
919
super(ProjectSetEdit, self).populate(req, ctx)
921
def get_default_data(self, req):
923
'group_size': self.context.max_students_per_group,
926
def save_object(self, req, data):
927
self.context.max_students_per_group = data['group_size']
930
class ProjectSetNew(BaseFormView):
931
"""A form to create a new project set."""
932
template = 'templates/projectset-new.html'
935
breadcrumb_text = "Projects"
939
return ProjectSetSchema()
941
def populate(self, req, ctx):
942
super(ProjectSetNew, self).populate(req, ctx)
944
def get_default_data(self, req):
947
def save_object(self, req, data):
948
new_set = ProjectSet()
949
new_set.offering = self.context
950
new_set.max_students_per_group = data['group_size']
951
req.store.add(new_set)
954
class Plugin(ViewPlugin, MediaPlugin):
955
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
956
offering_to_project, offering_to_projectset,
957
offering_to_enrolment)
959
subject_url, semester_url, offering_url, projectset_url, project_url,
962
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
963
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
964
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
965
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
966
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
967
(Subject, '+index', SubjectView),
968
(Subject, '+edit', SubjectEdit),
969
(Subject, '+new-offering', SubjectOfferingNew),
970
(Semester, '+edit', SemesterEdit),
971
(Offering, '+index', OfferingView),
972
(Offering, '+edit', OfferingEdit),
973
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
974
(Offering, ('+enrolments', '+index'), EnrolmentsView),
975
(Offering, ('+enrolments', '+new'), EnrolView),
976
(Enrolment, '+edit', EnrolmentEdit),
977
(Enrolment, '+delete', EnrolmentDelete),
978
(Offering, ('+projects', '+index'), OfferingProjectsView),
979
(Offering, ('+projects', '+new-set'), ProjectSetNew),
980
(ProjectSet, '+edit', ProjectSetEdit),
981
(ProjectSet, '+new', ProjectNew),
982
(Project, '+index', ProjectView),
983
(Project, '+edit', ProjectEdit),
984
(Project, '+delete', ProjectDelete),
985
(Project, ('+export', 'project-export.sh'),
986
ProjectBashExportView),
989
breadcrumbs = {Subject: SubjectBreadcrumb,
990
Offering: OfferingBreadcrumb,
991
User: UserBreadcrumb,
992
Project: ProjectBreadcrumb,
993
Enrolment: EnrolmentBreadcrumb,
997
('subjects', 'Subjects',
998
'View subject content and complete worksheets',
999
'subjects.png', 'subjects', 5)
1002
media = 'subject-media'