23
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
29
41
from ivle.webapp.base.xhtml import XHTMLView
30
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
from ivle.webapp.errors import NotFound
32
from ivle.database import Subject
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
33
47
from ivle import util
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
36
62
class SubjectsView(XHTMLView):
37
63
'''The view of the list of subjects.'''
38
template = 'subjects.html'
39
appname = 'subjects' # XXX
41
def authorize(self, req):
42
return req.user is not None
44
def populate(self, req, ctx):
45
enrolled_subjects = req.user.subjects
46
unenrolled_subjects = [subject for subject in
47
req.store.find(Subject)
48
if subject not in enrolled_subjects]
50
ctx['enrolled_subjects'] = []
51
ctx['other_subjects'] = []
53
req.content_type = "text/html"
54
req.write_html_head_foot = True
56
for subject in enrolled_subjects:
58
new_subj['name'] = subject.name
59
new_subj['url'] = subject.url
60
ctx['enrolled_subjects'].append(new_subj)
62
if len(unenrolled_subjects) > 0:
63
for subject in unenrolled_subjects:
65
new_subj['name'] = subject.name
66
new_subj['url'] = subject.url
67
ctx['other_subjects'].append(new_subj)
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
show_worksheet_marks = formencode.validators.StringBoolean(
392
class OfferingAdminSchema(OfferingSchema):
393
subject = formencode.All(
394
SubjectValidator(), formencode.validators.UnicodeString())
395
semester = formencode.All(
396
SemesterValidator(), formencode.validators.UnicodeString())
397
chained_validators = [OfferingUniquenessValidator()]
400
class OfferingEdit(BaseFormView):
401
"""A form to edit an offering's details."""
402
template = 'templates/offering-edit.html'
408
if self.req.user.admin:
409
return OfferingAdminSchema()
411
return OfferingSchema()
413
def populate(self, req, ctx):
414
super(OfferingEdit, self).populate(req, ctx)
415
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
416
ctx['semesters'] = req.store.find(Semester).order_by(
417
Semester.year, Semester.semester)
418
ctx['force_subject'] = None
420
def populate_state(self, state):
421
state.existing_offering = self.context
423
def get_default_data(self, req):
425
'subject': self.context.subject.short_name,
426
'semester': self.context.semester.year + '/' +
427
self.context.semester.semester,
428
'url': self.context.url,
429
'description': self.context.description,
430
'show_worksheet_marks': self.context.show_worksheet_marks,
433
def save_object(self, req, data):
435
self.context.subject = data['subject']
436
self.context.semester = data['semester']
437
self.context.description = data['description']
438
self.context.url = unicode(data['url']) if data['url'] else None
439
self.context.show_worksheet_marks = data['show_worksheet_marks']
443
class OfferingNew(BaseFormView):
444
"""A form to create an offering."""
445
template = 'templates/offering-new.html'
448
def authorize(self, req):
449
return req.user is not None and req.user.admin
453
return OfferingAdminSchema()
455
def populate(self, req, ctx):
456
super(OfferingNew, self).populate(req, ctx)
457
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
458
ctx['semesters'] = req.store.find(Semester).order_by(
459
Semester.year, Semester.semester)
460
ctx['force_subject'] = None
462
def populate_state(self, state):
463
state.existing_offering = None
465
def get_default_data(self, req):
468
def save_object(self, req, data):
469
new_offering = Offering()
470
new_offering.subject = data['subject']
471
new_offering.semester = data['semester']
472
new_offering.description = data['description']
473
new_offering.url = unicode(data['url']) if data['url'] else None
474
new_offering.show_worksheet_marks = data['show_worksheet_marks']
476
req.store.add(new_offering)
479
class SubjectOfferingNew(OfferingNew):
480
"""A form to create an offering for a given subject."""
481
# Identical to OfferingNew, except it forces the subject to be the subject
483
def populate(self, req, ctx):
484
super(SubjectOfferingNew, self).populate(req, ctx)
485
ctx['force_subject'] = self.context
487
class OfferingCloneWorksheetsSchema(formencode.Schema):
488
subject = formencode.All(
489
SubjectValidator(), formencode.validators.UnicodeString())
490
semester = formencode.All(
491
SemesterValidator(), formencode.validators.UnicodeString())
494
class OfferingCloneWorksheets(BaseFormView):
495
"""A form to clone worksheets from one offering to another."""
496
template = 'templates/offering-clone-worksheets.html'
499
def authorize(self, req):
500
return req.user is not None and req.user.admin
504
return OfferingCloneWorksheetsSchema()
506
def populate(self, req, ctx):
507
super(OfferingCloneWorksheets, self).populate(req, ctx)
508
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
509
ctx['semesters'] = req.store.find(Semester).order_by(
510
Semester.year, Semester.semester)
512
def get_default_data(self, req):
515
def save_object(self, req, data):
516
if self.context.worksheets.count() > 0:
518
"Cannot clone to target with existing worksheets.")
519
offering = req.store.find(
520
Offering, subject=data['subject'], semester=data['semester']).one()
522
raise BadRequest("No such offering.")
523
if offering.worksheets.count() == 0:
524
raise BadRequest("Source offering has no worksheets.")
526
self.context.clone_worksheets(offering)
530
class UserValidator(formencode.FancyValidator):
531
"""A FormEncode validator that turns a username into a user.
533
The state must have a 'store' attribute, which is the Storm store
535
def _to_python(self, value, state):
536
user = User.get_by_login(state.store, value)
540
raise formencode.Invalid('User does not exist', value, state)
543
class NoEnrolmentValidator(formencode.FancyValidator):
544
"""A FormEncode validator that ensures absence of an enrolment.
546
The state must have an 'offering' attribute.
548
def _to_python(self, value, state):
549
if state.offering.get_enrolment(value):
550
raise formencode.Invalid('User already enrolled', value, state)
554
class RoleEnrolmentValidator(formencode.FancyValidator):
555
"""A FormEncode validator that checks permission to enrol users with a
558
The state must have an 'offering' attribute.
560
def _to_python(self, value, state):
561
if (("enrol_" + value) not in
562
state.offering.get_permissions(state.user, state.config)):
563
raise formencode.Invalid('Not allowed to assign users that role',
568
class EnrolSchema(formencode.Schema):
569
user = formencode.All(NoEnrolmentValidator(), UserValidator())
570
role = formencode.All(formencode.validators.OneOf(
571
["lecturer", "tutor", "student"]),
572
RoleEnrolmentValidator(),
573
formencode.validators.UnicodeString())
576
class EnrolmentsView(XHTMLView):
577
"""A page which displays all users enrolled in an offering."""
578
template = 'templates/enrolments.html'
581
breadcrumb_text = 'Enrolments'
583
def populate(self, req, ctx):
585
ctx['offering'] = self.context
586
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
587
ctx['offering_perms'] = self.context.get_permissions(
588
req.user, req.config)
589
ctx['EnrolView'] = EnrolView
590
ctx['EnrolmentEdit'] = EnrolmentEdit
591
ctx['EnrolmentDelete'] = EnrolmentDelete
594
class EnrolView(XHTMLView):
595
"""A form to enrol a user in an offering."""
596
template = 'templates/enrol.html'
600
def filter(self, stream, ctx):
601
return stream | HTMLFormFiller(data=ctx['data'])
603
def populate(self, req, ctx):
604
if req.method == 'POST':
605
data = dict(req.get_fieldstorage())
607
validator = EnrolSchema()
608
req.offering = self.context # XXX: Getting into state.
609
data = validator.to_python(data, state=req)
610
self.context.enrol(data['user'], data['role'])
612
req.throw_redirect(req.uri)
613
except formencode.Invalid, e:
614
errors = e.unpack_errors()
619
ctx['data'] = data or {}
620
ctx['offering'] = self.context
621
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
622
ctx['errors'] = errors
623
# If all of the fields validated, set the global form error.
624
if isinstance(errors, basestring):
625
ctx['error_value'] = errors
628
class EnrolmentEditSchema(formencode.Schema):
629
role = formencode.All(formencode.validators.OneOf(
630
["lecturer", "tutor", "student"]),
631
RoleEnrolmentValidator(),
632
formencode.validators.UnicodeString())
635
class EnrolmentEdit(BaseFormView):
636
"""A form to alter an enrolment's role."""
637
template = 'templates/enrolment-edit.html'
641
def populate_state(self, state):
642
state.offering = self.context.offering
644
def get_default_data(self, req):
645
return {'role': self.context.role}
649
return EnrolmentEditSchema()
651
def save_object(self, req, data):
652
self.context.role = data['role']
654
def get_return_url(self, obj):
655
return self.req.publisher.generate(
656
self.context.offering, EnrolmentsView)
658
def populate(self, req, ctx):
659
super(EnrolmentEdit, self).populate(req, ctx)
660
ctx['offering_perms'] = self.context.offering.get_permissions(
661
req.user, req.config)
664
class EnrolmentDelete(XHTMLView):
665
"""A form to alter an enrolment's role."""
666
template = 'templates/enrolment-delete.html'
670
def populate(self, req, ctx):
671
# If POSTing, delete delete delete.
672
if req.method == 'POST':
673
self.context.delete()
675
req.throw_redirect(req.publisher.generate(
676
self.context.offering, EnrolmentsView))
678
ctx['enrolment'] = self.context
681
class OfferingProjectsView(XHTMLView):
682
"""View the projects for an offering."""
683
template = 'templates/offering_projects.html'
686
breadcrumb_text = 'Projects'
688
def populate(self, req, ctx):
689
self.plugin_styles[Plugin] = ["project.css"]
691
ctx['offering'] = self.context
692
ctx['projectsets'] = []
694
#Open the projectset Fragment, and render it for inclusion
695
#into the ProjectSets page
696
set_fragment = os.path.join(os.path.dirname(__file__),
697
"templates/projectset_fragment.html")
698
project_fragment = os.path.join(os.path.dirname(__file__),
699
"templates/project_fragment.html")
702
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
703
settmpl = self._loader.load(set_fragment)
706
setCtx['projectset'] = projectset
707
setCtx['projects'] = []
708
setCtx['GroupsView'] = GroupsView
709
setCtx['ProjectSetEdit'] = ProjectSetEdit
710
setCtx['ProjectNew'] = ProjectNew
713
projectset.projects.order_by(ivle.database.Project.deadline):
714
projecttmpl = self._loader.load(project_fragment)
715
projectCtx = Context()
716
projectCtx['req'] = req
717
projectCtx['project'] = project
718
projectCtx['ProjectEdit'] = ProjectEdit
719
projectCtx['ProjectDelete'] = ProjectDelete
721
setCtx['projects'].append(
722
projecttmpl.generate(projectCtx))
724
ctx['projectsets'].append(settmpl.generate(setCtx))
727
class ProjectView(XHTMLView):
728
"""View the submissions for a ProjectSet"""
729
template = "templates/project.html"
730
permission = "view_project_submissions"
733
def build_subversion_url(self, svnroot, submission):
734
princ = submission.assessed.principal
736
if isinstance(princ, User):
737
path = 'users/%s' % princ.login
739
path = 'groups/%s_%s_%s_%s' % (
740
princ.project_set.offering.subject.short_name,
741
princ.project_set.offering.semester.year,
742
princ.project_set.offering.semester.semester,
745
return urlparse.urljoin(
747
os.path.join(path, submission.path[1:] if
748
submission.path.startswith(os.sep) else
751
def populate(self, req, ctx):
752
self.plugin_styles[Plugin] = ["project.css"]
755
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
756
ctx['GroupsView'] = GroupsView
757
ctx['EnrolView'] = EnrolView
758
ctx['format_datetime'] = ivle.date.make_date_nice
759
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
760
ctx['build_subversion_url'] = self.build_subversion_url
761
ctx['svn_addr'] = req.config['urls']['svn_addr']
762
ctx['project'] = self.context
763
ctx['user'] = req.user
764
ctx['ProjectEdit'] = ProjectEdit
765
ctx['ProjectDelete'] = ProjectDelete
767
class ProjectUniquenessValidator(formencode.FancyValidator):
768
"""A FormEncode validator that checks that a project short_name is unique
771
The project referenced by state.existing_project is permitted to
772
hold that short_name. If any other project holds it, the input is rejected.
774
def _to_python(self, value, state):
775
if (state.store.find(
777
Project.short_name == unicode(value),
778
Project.project_set_id == ProjectSet.id,
779
ProjectSet.offering == state.offering).one() not in
780
(None, state.existing_project)):
781
raise formencode.Invalid(
782
"A project with that URL name already exists in this offering."
786
class ProjectSchema(formencode.Schema):
787
name = formencode.validators.UnicodeString(not_empty=True)
788
short_name = formencode.All(
789
URLNameValidator(not_empty=True),
790
ProjectUniquenessValidator())
791
deadline = DateTimeValidator(not_empty=True)
792
url = formencode.validators.URL(if_missing=None, not_empty=False)
793
synopsis = formencode.validators.UnicodeString(not_empty=True)
795
class ProjectEdit(BaseFormView):
796
"""A form to edit a project."""
797
template = 'templates/project-edit.html'
803
return ProjectSchema()
805
def populate(self, req, ctx):
806
super(ProjectEdit, self).populate(req, ctx)
807
ctx['projectset'] = self.context.project_set
809
def populate_state(self, state):
810
state.offering = self.context.project_set.offering
811
state.existing_project = self.context
813
def get_default_data(self, req):
815
'name': self.context.name,
816
'short_name': self.context.short_name,
817
'deadline': self.context.deadline,
818
'url': self.context.url,
819
'synopsis': self.context.synopsis,
822
def save_object(self, req, data):
823
self.context.name = data['name']
824
self.context.short_name = data['short_name']
825
self.context.deadline = data['deadline']
826
self.context.url = unicode(data['url']) if data['url'] else None
827
self.context.synopsis = data['synopsis']
830
class ProjectNew(BaseFormView):
831
"""A form to create a new project."""
832
template = 'templates/project-new.html'
838
return ProjectSchema()
840
def populate(self, req, ctx):
841
super(ProjectNew, self).populate(req, ctx)
842
ctx['projectset'] = self.context
844
def populate_state(self, state):
845
state.offering = self.context.offering
846
state.existing_project = None
848
def get_default_data(self, req):
851
def save_object(self, req, data):
852
new_project = Project()
853
new_project.project_set = self.context
854
new_project.name = data['name']
855
new_project.short_name = data['short_name']
856
new_project.deadline = data['deadline']
857
new_project.url = unicode(data['url']) if data['url'] else None
858
new_project.synopsis = data['synopsis']
859
req.store.add(new_project)
862
class ProjectDelete(XHTMLView):
863
"""A form to delete a project."""
864
template = 'templates/project-delete.html'
868
def populate(self, req, ctx):
869
# If post, delete the project, or display a message explaining that
870
# the project cannot be deleted
871
if self.context.can_delete:
872
if req.method == 'POST':
873
self.context.delete()
874
self.template = 'templates/project-deleted.html'
877
self.template = 'templates/project-undeletable.html'
879
# If get and can delete, display a delete confirmation page
881
# Variables for the template
883
ctx['project'] = self.context
884
ctx['OfferingProjectsView'] = OfferingProjectsView
886
class ProjectSetSchema(formencode.Schema):
887
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
889
class ProjectSetEdit(BaseFormView):
890
"""A form to edit a project set."""
891
template = 'templates/projectset-edit.html'
897
return ProjectSetSchema()
899
def populate(self, req, ctx):
900
super(ProjectSetEdit, self).populate(req, ctx)
902
def get_default_data(self, req):
904
'group_size': self.context.max_students_per_group,
907
def save_object(self, req, data):
908
self.context.max_students_per_group = data['group_size']
911
class ProjectSetNew(BaseFormView):
912
"""A form to create a new project set."""
913
template = 'templates/projectset-new.html'
916
breadcrumb_text = "Projects"
920
return ProjectSetSchema()
922
def populate(self, req, ctx):
923
super(ProjectSetNew, self).populate(req, ctx)
925
def get_default_data(self, req):
928
def save_object(self, req, data):
929
new_set = ProjectSet()
930
new_set.offering = self.context
931
new_set.max_students_per_group = data['group_size']
932
req.store.add(new_set)
70
935
class Plugin(ViewPlugin, MediaPlugin):
72
('subjects/', SubjectsView),
936
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
937
offering_to_project, offering_to_projectset,
938
offering_to_enrolment)
940
subject_url, semester_url, offering_url, projectset_url, project_url,
943
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
944
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
945
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
946
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
947
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
948
(Subject, '+index', SubjectView),
949
(Subject, '+edit', SubjectEdit),
950
(Subject, '+new-offering', SubjectOfferingNew),
951
(Semester, '+edit', SemesterEdit),
952
(Offering, '+index', OfferingView),
953
(Offering, '+edit', OfferingEdit),
954
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
955
(Offering, ('+enrolments', '+index'), EnrolmentsView),
956
(Offering, ('+enrolments', '+new'), EnrolView),
957
(Enrolment, '+edit', EnrolmentEdit),
958
(Enrolment, '+delete', EnrolmentDelete),
959
(Offering, ('+projects', '+index'), OfferingProjectsView),
960
(Offering, ('+projects', '+new-set'), ProjectSetNew),
961
(ProjectSet, '+edit', ProjectSetEdit),
962
(ProjectSet, '+new', ProjectNew),
963
(Project, '+index', ProjectView),
964
(Project, '+edit', ProjectEdit),
965
(Project, '+delete', ProjectDelete),
968
breadcrumbs = {Subject: SubjectBreadcrumb,
969
Offering: OfferingBreadcrumb,
970
User: UserBreadcrumb,
971
Project: ProjectBreadcrumb,
972
Enrolment: EnrolmentBreadcrumb,
76
('subjects', 'Subjects', 'Announcements and information about the '
77
'subjects you are enrolled in.', 'subjects.png', 'subjects', 5)
976
('subjects', 'Subjects',
977
'View subject content and complete worksheets',
978
'subjects.png', 'subjects', 5)
80
981
media = 'subject-media'