23
23
# A sample / testing application for IVLE.
29
from common import util
32
"""Handler for the Subjects application. Links to subject home pages."""
34
req.styles = ["media/subjects/subjects.css"]
36
handle_toplevel_menu(req)
38
handle_subject_page(req, req.path)
40
def handle_toplevel_menu(req):
41
# This is represented as a directory. Redirect and add a slash if it is
43
if req.uri[-1] != '/':
44
req.throw_redirect(req.uri + '/')
46
# Get list of subjects
47
# TODO: Fetch from DB. For now, just get directory listing
49
subjects = os.listdir(util.make_local_path(os.path.join('media',
52
req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
53
"There are is no subject homepages directory.")
56
req.content_type = "text/html"
57
req.write_html_head_foot = True
58
req.write('<div id="ivle_padding">\n')
59
req.write("<h2>IVLE Subject Homepages</h2>\n")
60
req.write("<h2>Subjects</h2>\n<ul>\n")
61
for subject in subjects:
62
req.write(' <li><a href="%s">%s</a></li>\n'
63
% (urllib.quote(subject) + '/', cgi.escape(subject)))
67
def handle_subject_page(req, path):
68
req.content_type = "text/html"
69
req.write_html_head_foot = True # Have dispatch print head and foot
71
# Just make the iframe pointing to media/subjects
72
serve_loc = util.make_path(os.path.join('media', 'subjects', path))
73
req.write('<iframe src="%s"></iframe>'
74
% urllib.quote(serve_loc))
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 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'], url_name=value['url_name']
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
code = formencode.validators.UnicodeString()
207
url_name = URLNameValidator()
208
display_name = formencode.validators.UnicodeString()
209
state = formencode.All(
210
formencode.validators.OneOf(["past", "current", "future"]),
211
formencode.validators.UnicodeString())
212
chained_validators = [SemesterUniquenessValidator()]
215
class SemesterFormView(BaseFormView):
218
def authorize(self, req):
219
return req.user is not None and req.user.admin
223
return SemesterSchema()
225
def get_return_url(self, obj):
226
return '/subjects/+manage'
229
class SemesterNew(SemesterFormView):
230
"""A form to create a semester."""
231
template = 'templates/semester-new.html'
234
def populate_state(self, state):
235
state.existing_semester = None
237
def get_default_data(self, req):
240
def save_object(self, req, data):
241
new_semester = Semester()
242
new_semester.year = data['year']
243
new_semester.code = data['code']
244
new_semester.url_name = data['url_name']
245
new_semester.display_name = data['display_name']
246
new_semester.state = data['state']
248
req.store.add(new_semester)
252
class SemesterEdit(SemesterFormView):
253
"""A form to edit a semester."""
254
template = 'templates/semester-edit.html'
256
def populate_state(self, state):
257
state.existing_semester = self.context
259
def get_default_data(self, req):
261
'year': self.context.year,
262
'code': self.context.code,
263
'url_name': self.context.url_name,
264
'display_name': self.context.display_name,
265
'state': self.context.state,
268
def save_object(self, req, data):
269
self.context.year = data['year']
270
self.context.code = data['code']
271
self.context.url_name = data['url_name']
272
self.context.display_name = data['display_name']
273
self.context.state = data['state']
277
class SubjectView(XHTMLView):
278
'''The view of the list of offerings in a given subject.'''
279
template = 'templates/subject.html'
282
def authorize(self, req):
283
return req.user is not None
285
def populate(self, req, ctx):
286
ctx['context'] = self.context
288
ctx['user'] = req.user
289
ctx['offerings'] = list(self.context.offerings)
290
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
291
ctx['SubjectEdit'] = SubjectEdit
292
ctx['SubjectOfferingNew'] = SubjectOfferingNew
295
class OfferingView(XHTMLView):
296
"""The home page of an offering."""
297
template = 'templates/offering.html'
301
def populate(self, req, ctx):
302
# Need the worksheet result styles.
303
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
304
ctx['context'] = self.context
306
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
307
ctx['format_submission_principal'] = util.format_submission_principal
308
ctx['format_datetime'] = ivle.date.make_date_nice
309
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
310
ctx['OfferingEdit'] = OfferingEdit
311
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
312
ctx['GroupsView'] = GroupsView
313
ctx['EnrolmentsView'] = EnrolmentsView
314
ctx['Project'] = ivle.database.Project
316
# As we go, calculate the total score for this subject
317
# (Assessable worksheets only, mandatory problems only)
319
ctx['worksheets'], problems_total, problems_done = (
320
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
321
req.config, req.store, req.user, self.context,
322
as_of=self.context.worksheet_cutoff))
324
ctx['exercises_total'] = problems_total
325
ctx['exercises_done'] = problems_done
326
if problems_total > 0:
327
if problems_done >= problems_total:
328
ctx['worksheets_complete_class'] = "complete"
329
elif problems_done > 0:
330
ctx['worksheets_complete_class'] = "semicomplete"
332
ctx['worksheets_complete_class'] = "incomplete"
333
# Calculate the final percentage and mark for the subject
334
(ctx['exercises_pct'], ctx['worksheet_mark'],
335
ctx['worksheet_max_mark']) = (
336
ivle.worksheet.utils.calculate_mark(
337
problems_done, problems_total))
340
class SubjectValidator(formencode.FancyValidator):
341
"""A FormEncode validator that turns a subject name into a subject.
343
The state must have a 'store' attribute, which is the Storm store
346
def _to_python(self, value, state):
347
subject = state.store.find(Subject, short_name=value).one()
351
raise formencode.Invalid('Subject does not exist', value, state)
354
class SemesterValidator(formencode.FancyValidator):
355
"""A FormEncode validator that turns a string into a semester.
357
The string should be of the form 'year/semester', eg. '2009/1'.
359
The state must have a 'store' attribute, which is the Storm store
362
def _to_python(self, value, state):
364
year, semester = value.split('/')
366
year = semester = None
368
semester = state.store.find(
369
Semester, year=year, url_name=semester).one()
373
raise formencode.Invalid('Semester does not exist', value, state)
376
class OfferingUniquenessValidator(formencode.FancyValidator):
377
"""A FormEncode validator that checks that an offering is unique.
379
There cannot be more than one offering in the same year and semester.
381
The offering referenced by state.existing_offering is permitted to
382
hold that year and semester tuple. If any other object holds it, the
385
def _to_python(self, value, state):
386
if (state.store.find(
387
Offering, subject=value['subject'],
388
semester=value['semester']).one() not in
389
(None, state.existing_offering)):
390
raise formencode.Invalid(
391
'Offering already exists', value, state)
395
class OfferingSchema(formencode.Schema):
396
description = formencode.validators.UnicodeString(
397
if_missing=None, not_empty=False)
398
url = formencode.validators.URL(if_missing=None, not_empty=False)
399
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
400
show_worksheet_marks = formencode.validators.StringBoolean(
404
class OfferingAdminSchema(OfferingSchema):
405
subject = formencode.All(
406
SubjectValidator(), formencode.validators.UnicodeString())
407
semester = formencode.All(
408
SemesterValidator(), formencode.validators.UnicodeString())
409
chained_validators = [OfferingUniquenessValidator()]
412
class OfferingEdit(BaseFormView):
413
"""A form to edit an offering's details."""
414
template = 'templates/offering-edit.html'
420
if self.req.user.admin:
421
return OfferingAdminSchema()
423
return OfferingSchema()
425
def populate(self, req, ctx):
426
super(OfferingEdit, self).populate(req, ctx)
427
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
428
ctx['semesters'] = req.store.find(Semester).order_by(
429
Semester.year, Semester.display_name)
430
ctx['force_subject'] = None
432
def populate_state(self, state):
433
state.existing_offering = self.context
435
def get_default_data(self, req):
437
'subject': self.context.subject.short_name,
438
'semester': self.context.semester.year + '/' +
439
self.context.semester.url_name,
440
'url': self.context.url,
441
'description': self.context.description,
442
'worksheet_cutoff': self.context.worksheet_cutoff,
443
'show_worksheet_marks': self.context.show_worksheet_marks,
446
def save_object(self, req, data):
448
self.context.subject = data['subject']
449
self.context.semester = data['semester']
450
self.context.description = data['description']
451
self.context.url = unicode(data['url']) if data['url'] else None
452
self.context.worksheet_cutoff = data['worksheet_cutoff']
453
self.context.show_worksheet_marks = data['show_worksheet_marks']
457
class OfferingNew(BaseFormView):
458
"""A form to create an offering."""
459
template = 'templates/offering-new.html'
462
def authorize(self, req):
463
return req.user is not None and req.user.admin
467
return OfferingAdminSchema()
469
def populate(self, req, ctx):
470
super(OfferingNew, self).populate(req, ctx)
471
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
472
ctx['semesters'] = req.store.find(Semester).order_by(
473
Semester.year, Semester.display_name)
474
ctx['force_subject'] = None
476
def populate_state(self, state):
477
state.existing_offering = None
479
def get_default_data(self, req):
482
def save_object(self, req, data):
483
new_offering = Offering()
484
new_offering.subject = data['subject']
485
new_offering.semester = data['semester']
486
new_offering.description = data['description']
487
new_offering.url = unicode(data['url']) if data['url'] else None
488
new_offering.worksheet_cutoff = data['worksheet_cutoff']
489
new_offering.show_worksheet_marks = data['show_worksheet_marks']
491
req.store.add(new_offering)
494
class SubjectOfferingNew(OfferingNew):
495
"""A form to create an offering for a given subject."""
496
# Identical to OfferingNew, except it forces the subject to be the subject
498
def populate(self, req, ctx):
499
super(SubjectOfferingNew, self).populate(req, ctx)
500
ctx['force_subject'] = self.context
502
class OfferingCloneWorksheetsSchema(formencode.Schema):
503
subject = formencode.All(
504
SubjectValidator(), formencode.validators.UnicodeString())
505
semester = formencode.All(
506
SemesterValidator(), formencode.validators.UnicodeString())
509
class OfferingCloneWorksheets(BaseFormView):
510
"""A form to clone worksheets from one offering to another."""
511
template = 'templates/offering-clone-worksheets.html'
514
def authorize(self, req):
515
return req.user is not None and req.user.admin
519
return OfferingCloneWorksheetsSchema()
521
def populate(self, req, ctx):
522
super(OfferingCloneWorksheets, self).populate(req, ctx)
523
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
524
ctx['semesters'] = req.store.find(Semester).order_by(
525
Semester.year, Semester.display_name)
527
def get_default_data(self, req):
530
def save_object(self, req, data):
531
if self.context.worksheets.count() > 0:
533
"Cannot clone to target with existing worksheets.")
534
offering = req.store.find(
535
Offering, subject=data['subject'], semester=data['semester']).one()
537
raise BadRequest("No such offering.")
538
if offering.worksheets.count() == 0:
539
raise BadRequest("Source offering has no worksheets.")
541
self.context.clone_worksheets(offering)
545
class UserValidator(formencode.FancyValidator):
546
"""A FormEncode validator that turns a username into a user.
548
The state must have a 'store' attribute, which is the Storm store
550
def _to_python(self, value, state):
551
user = User.get_by_login(state.store, value)
555
raise formencode.Invalid('User does not exist', value, state)
558
class NoEnrolmentValidator(formencode.FancyValidator):
559
"""A FormEncode validator that ensures absence of an enrolment.
561
The state must have an 'offering' attribute.
563
def _to_python(self, value, state):
564
if state.offering.get_enrolment(value):
565
raise formencode.Invalid('User already enrolled', value, state)
569
class RoleEnrolmentValidator(formencode.FancyValidator):
570
"""A FormEncode validator that checks permission to enrol users with a
573
The state must have an 'offering' attribute.
575
def _to_python(self, value, state):
576
if (("enrol_" + value) not in
577
state.offering.get_permissions(state.user, state.config)):
578
raise formencode.Invalid('Not allowed to assign users that role',
583
class EnrolSchema(formencode.Schema):
584
user = formencode.All(NoEnrolmentValidator(), UserValidator())
585
role = formencode.All(formencode.validators.OneOf(
586
["lecturer", "tutor", "student"]),
587
RoleEnrolmentValidator(),
588
formencode.validators.UnicodeString())
591
class EnrolmentsView(XHTMLView):
592
"""A page which displays all users enrolled in an offering."""
593
template = 'templates/enrolments.html'
596
breadcrumb_text = 'Enrolments'
598
def populate(self, req, ctx):
600
ctx['offering'] = self.context
601
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
602
ctx['offering_perms'] = self.context.get_permissions(
603
req.user, req.config)
604
ctx['EnrolView'] = EnrolView
605
ctx['EnrolmentEdit'] = EnrolmentEdit
606
ctx['EnrolmentDelete'] = EnrolmentDelete
609
class EnrolView(XHTMLView):
610
"""A form to enrol a user in an offering."""
611
template = 'templates/enrol.html'
615
def filter(self, stream, ctx):
616
return stream | HTMLFormFiller(data=ctx['data'])
618
def populate(self, req, ctx):
619
if req.method == 'POST':
620
data = dict(req.get_fieldstorage())
622
validator = EnrolSchema()
623
req.offering = self.context # XXX: Getting into state.
624
data = validator.to_python(data, state=req)
625
self.context.enrol(data['user'], data['role'])
627
req.throw_redirect(req.uri)
628
except formencode.Invalid, e:
629
errors = e.unpack_errors()
634
ctx['data'] = data or {}
635
ctx['offering'] = self.context
636
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
637
ctx['errors'] = errors
638
# If all of the fields validated, set the global form error.
639
if isinstance(errors, basestring):
640
ctx['error_value'] = errors
643
class EnrolmentEditSchema(formencode.Schema):
644
role = formencode.All(formencode.validators.OneOf(
645
["lecturer", "tutor", "student"]),
646
RoleEnrolmentValidator(),
647
formencode.validators.UnicodeString())
650
class EnrolmentEdit(BaseFormView):
651
"""A form to alter an enrolment's role."""
652
template = 'templates/enrolment-edit.html'
656
def populate_state(self, state):
657
state.offering = self.context.offering
659
def get_default_data(self, req):
660
return {'role': self.context.role}
664
return EnrolmentEditSchema()
666
def save_object(self, req, data):
667
self.context.role = data['role']
669
def get_return_url(self, obj):
670
return self.req.publisher.generate(
671
self.context.offering, EnrolmentsView)
673
def populate(self, req, ctx):
674
super(EnrolmentEdit, self).populate(req, ctx)
675
ctx['offering_perms'] = self.context.offering.get_permissions(
676
req.user, req.config)
679
class EnrolmentDelete(XHTMLView):
680
"""A form to alter an enrolment's role."""
681
template = 'templates/enrolment-delete.html'
685
def populate(self, req, ctx):
686
# If POSTing, delete delete delete.
687
if req.method == 'POST':
688
self.context.delete()
690
req.throw_redirect(req.publisher.generate(
691
self.context.offering, EnrolmentsView))
693
ctx['enrolment'] = self.context
696
class OfferingProjectsView(XHTMLView):
697
"""View the projects for an offering."""
698
template = 'templates/offering_projects.html'
701
breadcrumb_text = 'Projects'
703
def populate(self, req, ctx):
704
self.plugin_styles[Plugin] = ["project.css"]
706
ctx['offering'] = self.context
707
ctx['projectsets'] = []
709
#Open the projectset Fragment, and render it for inclusion
710
#into the ProjectSets page
711
set_fragment = os.path.join(os.path.dirname(__file__),
712
"templates/projectset_fragment.html")
713
project_fragment = os.path.join(os.path.dirname(__file__),
714
"templates/project_fragment.html")
717
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
718
settmpl = self._loader.load(set_fragment)
721
setCtx['projectset'] = projectset
722
setCtx['projects'] = []
723
setCtx['GroupsView'] = GroupsView
724
setCtx['ProjectSetEdit'] = ProjectSetEdit
725
setCtx['ProjectNew'] = ProjectNew
728
projectset.projects.order_by(ivle.database.Project.deadline):
729
projecttmpl = self._loader.load(project_fragment)
730
projectCtx = Context()
731
projectCtx['req'] = req
732
projectCtx['project'] = project
733
projectCtx['ProjectEdit'] = ProjectEdit
734
projectCtx['ProjectDelete'] = ProjectDelete
736
setCtx['projects'].append(
737
projecttmpl.generate(projectCtx))
739
ctx['projectsets'].append(settmpl.generate(setCtx))
742
class ProjectView(XHTMLView):
743
"""View the submissions for a ProjectSet"""
744
template = "templates/project.html"
745
permission = "view_project_submissions"
748
def populate(self, req, ctx):
749
self.plugin_styles[Plugin] = ["project.css"]
752
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
753
ctx['GroupsView'] = GroupsView
754
ctx['EnrolView'] = EnrolView
755
ctx['format_datetime'] = ivle.date.make_date_nice
756
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
757
ctx['project'] = self.context
758
ctx['user'] = req.user
759
ctx['ProjectEdit'] = ProjectEdit
760
ctx['ProjectDelete'] = ProjectDelete
761
ctx['ProjectExport'] = ProjectBashExportView
763
class ProjectBashExportView(TextView):
764
"""Produce a Bash script for exporting projects"""
765
template = "templates/project-export.sh"
766
content_type = "text/x-sh"
767
permission = "view_project_submissions"
769
def populate(self, req, ctx):
771
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
772
ctx['format_datetime'] = ivle.date.make_date_nice
773
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
774
ctx['project'] = self.context
775
ctx['user'] = req.user
776
ctx['now'] = datetime.datetime.now()
777
ctx['format_datetime'] = ivle.date.make_date_nice
778
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
780
class ProjectUniquenessValidator(formencode.FancyValidator):
781
"""A FormEncode validator that checks that a project short_name is unique
784
The project referenced by state.existing_project is permitted to
785
hold that short_name. If any other project holds it, the input is rejected.
787
def _to_python(self, value, state):
788
if (state.store.find(
790
Project.short_name == unicode(value),
791
Project.project_set_id == ProjectSet.id,
792
ProjectSet.offering == state.offering).one() not in
793
(None, state.existing_project)):
794
raise formencode.Invalid(
795
"A project with that URL name already exists in this offering."
799
class ProjectSchema(formencode.Schema):
800
name = formencode.validators.UnicodeString(not_empty=True)
801
short_name = formencode.All(
802
URLNameValidator(not_empty=True),
803
ProjectUniquenessValidator())
804
deadline = DateTimeValidator(not_empty=True)
805
url = formencode.validators.URL(if_missing=None, not_empty=False)
806
synopsis = formencode.validators.UnicodeString(not_empty=True)
808
class ProjectEdit(BaseFormView):
809
"""A form to edit a project."""
810
template = 'templates/project-edit.html'
816
return ProjectSchema()
818
def populate(self, req, ctx):
819
super(ProjectEdit, self).populate(req, ctx)
820
ctx['projectset'] = self.context.project_set
822
def populate_state(self, state):
823
state.offering = self.context.project_set.offering
824
state.existing_project = self.context
826
def get_default_data(self, req):
828
'name': self.context.name,
829
'short_name': self.context.short_name,
830
'deadline': self.context.deadline,
831
'url': self.context.url,
832
'synopsis': self.context.synopsis,
835
def save_object(self, req, data):
836
self.context.name = data['name']
837
self.context.short_name = data['short_name']
838
self.context.deadline = data['deadline']
839
self.context.url = unicode(data['url']) if data['url'] else None
840
self.context.synopsis = data['synopsis']
843
class ProjectNew(BaseFormView):
844
"""A form to create a new project."""
845
template = 'templates/project-new.html'
851
return ProjectSchema()
853
def populate(self, req, ctx):
854
super(ProjectNew, self).populate(req, ctx)
855
ctx['projectset'] = self.context
857
def populate_state(self, state):
858
state.offering = self.context.offering
859
state.existing_project = None
861
def get_default_data(self, req):
864
def save_object(self, req, data):
865
new_project = Project()
866
new_project.project_set = self.context
867
new_project.name = data['name']
868
new_project.short_name = data['short_name']
869
new_project.deadline = data['deadline']
870
new_project.url = unicode(data['url']) if data['url'] else None
871
new_project.synopsis = data['synopsis']
872
req.store.add(new_project)
875
class ProjectDelete(XHTMLView):
876
"""A form to delete a project."""
877
template = 'templates/project-delete.html'
881
def populate(self, req, ctx):
882
# If post, delete the project, or display a message explaining that
883
# the project cannot be deleted
884
if self.context.can_delete:
885
if req.method == 'POST':
886
self.context.delete()
887
self.template = 'templates/project-deleted.html'
890
self.template = 'templates/project-undeletable.html'
892
# If get and can delete, display a delete confirmation page
894
# Variables for the template
896
ctx['project'] = self.context
897
ctx['OfferingProjectsView'] = OfferingProjectsView
899
class ProjectSetSchema(formencode.Schema):
900
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
902
class ProjectSetEdit(BaseFormView):
903
"""A form to edit a project set."""
904
template = 'templates/projectset-edit.html'
910
return ProjectSetSchema()
912
def populate(self, req, ctx):
913
super(ProjectSetEdit, self).populate(req, ctx)
915
def get_default_data(self, req):
917
'group_size': self.context.max_students_per_group,
920
def save_object(self, req, data):
921
self.context.max_students_per_group = data['group_size']
924
class ProjectSetNew(BaseFormView):
925
"""A form to create a new project set."""
926
template = 'templates/projectset-new.html'
929
breadcrumb_text = "Projects"
933
return ProjectSetSchema()
935
def populate(self, req, ctx):
936
super(ProjectSetNew, self).populate(req, ctx)
938
def get_default_data(self, req):
941
def save_object(self, req, data):
942
new_set = ProjectSet()
943
new_set.offering = self.context
944
new_set.max_students_per_group = data['group_size']
945
req.store.add(new_set)
948
class Plugin(ViewPlugin, MediaPlugin):
949
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
950
offering_to_project, offering_to_projectset,
951
offering_to_enrolment)
953
subject_url, semester_url, offering_url, projectset_url, project_url,
956
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
957
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
958
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
959
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
960
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
961
(Subject, '+index', SubjectView),
962
(Subject, '+edit', SubjectEdit),
963
(Subject, '+new-offering', SubjectOfferingNew),
964
(Semester, '+edit', SemesterEdit),
965
(Offering, '+index', OfferingView),
966
(Offering, '+edit', OfferingEdit),
967
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
968
(Offering, ('+enrolments', '+index'), EnrolmentsView),
969
(Offering, ('+enrolments', '+new'), EnrolView),
970
(Enrolment, '+edit', EnrolmentEdit),
971
(Enrolment, '+delete', EnrolmentDelete),
972
(Offering, ('+projects', '+index'), OfferingProjectsView),
973
(Offering, ('+projects', '+new-set'), ProjectSetNew),
974
(ProjectSet, '+edit', ProjectSetEdit),
975
(ProjectSet, '+new', ProjectNew),
976
(Project, '+index', ProjectView),
977
(Project, '+edit', ProjectEdit),
978
(Project, '+delete', ProjectDelete),
979
(Project, ('+export', 'project-export.sh'),
980
ProjectBashExportView),
983
breadcrumbs = {Subject: SubjectBreadcrumb,
984
Offering: OfferingBreadcrumb,
985
User: UserBreadcrumb,
986
Project: ProjectBreadcrumb,
987
Enrolment: EnrolmentBreadcrumb,
991
('subjects', 'Subjects',
992
'View subject content and complete worksheets',
993
'subjects.png', 'subjects', 5)
996
media = 'subject-media'