23
23
# A sample / testing application for IVLE.
32
from storm.locals import Desc, Store
29
from storm.locals import Desc
34
31
from genshi.filters import HTMLFormFiller
35
from genshi.template import Context
32
from genshi.template import Context, TemplateLoader
37
import formencode.validators
39
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
35
from ivle.webapp.base.xhtml import XHTMLView
41
36
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
37
from ivle.webapp.errors import NotFound
47
39
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
48
40
ProjectSet, Project, ProjectSubmission
49
41
from ivle import util
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
43
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
45
from ivle.webapp.admin.offeringservice import OfferingRESTView
64
48
class SubjectsView(XHTMLView):
65
49
'''The view of the list of subjects.'''
66
50
template = 'templates/subjects.html'
68
breadcrumb_text = "Subjects"
70
53
def authorize(self, req):
71
54
return req.user is not None
73
56
def populate(self, req, ctx):
75
57
ctx['user'] = req.user
76
58
ctx['semesters'] = []
78
59
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
79
60
Desc(Semester.semester)):
81
# For admins, show all subjects in the system
82
offerings = list(semester.offerings.find())
84
offerings = [enrolment.offering for enrolment in
85
semester.enrolments.find(user=req.user)]
87
ctx['semesters'].append((semester, offerings))
90
class SubjectsManage(XHTMLView):
91
'''Subject management view.'''
92
template = 'templates/subjects-manage.html'
95
def authorize(self, req):
96
return req.user is not None and req.user.admin
98
def populate(self, req, ctx):
100
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
101
ctx['SubjectView'] = SubjectView
102
ctx['SubjectEdit'] = SubjectEdit
103
ctx['SemesterEdit'] = SemesterEdit
105
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
106
ctx['semesters'] = req.store.find(Semester).order_by(
107
Semester.year, Semester.semester)
110
class 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'], semester=value['semester']
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
semester = URLNameValidator()
213
state = formencode.All(
214
formencode.validators.OneOf(["past", "current", "future"]),
215
formencode.validators.UnicodeString())
216
chained_validators = [SemesterUniquenessValidator()]
219
class SemesterFormView(BaseFormView):
222
def authorize(self, req):
223
return req.user is not None and req.user.admin
227
return SemesterSchema()
229
def get_return_url(self, obj):
230
return '/subjects/+manage'
233
class SemesterNew(SemesterFormView):
234
"""A form to create a semester."""
235
template = 'templates/semester-new.html'
238
def populate_state(self, state):
239
state.existing_semester = None
241
def get_default_data(self, req):
244
def save_object(self, req, data):
245
new_semester = Semester()
246
new_semester.year = data['year']
247
new_semester.semester = data['semester']
248
new_semester.state = data['state']
250
req.store.add(new_semester)
254
class SemesterEdit(SemesterFormView):
255
"""A form to edit a semester."""
256
template = 'templates/semester-edit.html'
258
def populate_state(self, state):
259
state.existing_semester = self.context
261
def get_default_data(self, req):
263
'year': self.context.year,
264
'semester': self.context.semester,
265
'state': self.context.state,
268
def save_object(self, req, data):
269
self.context.year = data['year']
270
self.context.semester = data['semester']
271
self.context.state = data['state']
275
class SubjectView(XHTMLView):
276
'''The view of the list of offerings in a given subject.'''
277
template = 'templates/subject.html'
280
def authorize(self, req):
281
return req.user is not None
283
def populate(self, req, ctx):
284
ctx['context'] = self.context
286
ctx['user'] = req.user
287
ctx['offerings'] = list(self.context.offerings)
288
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
289
ctx['SubjectEdit'] = SubjectEdit
290
ctx['SubjectOfferingNew'] = SubjectOfferingNew
293
class OfferingView(XHTMLView):
294
"""The home page of an offering."""
295
template = 'templates/offering.html'
299
def populate(self, req, ctx):
300
# Need the worksheet result styles.
301
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
302
ctx['context'] = self.context
304
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
305
ctx['format_submission_principal'] = util.format_submission_principal
306
ctx['format_datetime'] = ivle.date.make_date_nice
307
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
308
ctx['OfferingEdit'] = OfferingEdit
309
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
310
ctx['GroupsView'] = GroupsView
311
ctx['EnrolmentsView'] = EnrolmentsView
312
ctx['Project'] = ivle.database.Project
314
# As we go, calculate the total score for this subject
315
# (Assessable worksheets only, mandatory problems only)
317
ctx['worksheets'], problems_total, problems_done = (
318
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
319
req.config, req.store, req.user, self.context,
320
as_of=self.context.worksheet_cutoff))
322
ctx['exercises_total'] = problems_total
323
ctx['exercises_done'] = problems_done
324
if problems_total > 0:
325
if problems_done >= problems_total:
326
ctx['worksheets_complete_class'] = "complete"
327
elif problems_done > 0:
328
ctx['worksheets_complete_class'] = "semicomplete"
330
ctx['worksheets_complete_class'] = "incomplete"
331
# Calculate the final percentage and mark for the subject
332
(ctx['exercises_pct'], ctx['worksheet_mark'],
333
ctx['worksheet_max_mark']) = (
334
ivle.worksheet.utils.calculate_mark(
335
problems_done, problems_total))
338
class SubjectValidator(formencode.FancyValidator):
339
"""A FormEncode validator that turns a subject name into a subject.
341
The state must have a 'store' attribute, which is the Storm store
344
def _to_python(self, value, state):
345
subject = state.store.find(Subject, short_name=value).one()
349
raise formencode.Invalid('Subject does not exist', value, state)
352
class SemesterValidator(formencode.FancyValidator):
353
"""A FormEncode validator that turns a string into a semester.
355
The string should be of the form 'year/semester', eg. '2009/1'.
357
The state must have a 'store' attribute, which is the Storm store
360
def _to_python(self, value, state):
362
year, semester = value.split('/')
364
year = semester = None
366
semester = state.store.find(
367
Semester, year=year, semester=semester).one()
371
raise formencode.Invalid('Semester does not exist', value, state)
374
class OfferingUniquenessValidator(formencode.FancyValidator):
375
"""A FormEncode validator that checks that an offering is unique.
377
There cannot be more than one offering in the same year and semester.
379
The offering referenced by state.existing_offering is permitted to
380
hold that year and semester tuple. If any other object holds it, the
383
def _to_python(self, value, state):
384
if (state.store.find(
385
Offering, subject=value['subject'],
386
semester=value['semester']).one() not in
387
(None, state.existing_offering)):
388
raise formencode.Invalid(
389
'Offering already exists', value, state)
393
class OfferingSchema(formencode.Schema):
394
description = formencode.validators.UnicodeString(
395
if_missing=None, not_empty=False)
396
url = formencode.validators.URL(if_missing=None, not_empty=False)
397
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
398
show_worksheet_marks = formencode.validators.StringBoolean(
402
class OfferingAdminSchema(OfferingSchema):
403
subject = formencode.All(
404
SubjectValidator(), formencode.validators.UnicodeString())
405
semester = formencode.All(
406
SemesterValidator(), formencode.validators.UnicodeString())
407
chained_validators = [OfferingUniquenessValidator()]
410
class OfferingEdit(BaseFormView):
411
"""A form to edit an offering's details."""
412
template = 'templates/offering-edit.html'
418
if self.req.user.admin:
419
return OfferingAdminSchema()
421
return OfferingSchema()
423
def populate(self, req, ctx):
424
super(OfferingEdit, self).populate(req, ctx)
425
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
426
ctx['semesters'] = req.store.find(Semester).order_by(
427
Semester.year, Semester.semester)
428
ctx['force_subject'] = None
430
def populate_state(self, state):
431
state.existing_offering = self.context
433
def get_default_data(self, req):
435
'subject': self.context.subject.short_name,
436
'semester': self.context.semester.year + '/' +
437
self.context.semester.semester,
438
'url': self.context.url,
439
'description': self.context.description,
440
'worksheet_cutoff': self.context.worksheet_cutoff,
441
'show_worksheet_marks': self.context.show_worksheet_marks,
444
def save_object(self, req, data):
446
self.context.subject = data['subject']
447
self.context.semester = data['semester']
448
self.context.description = data['description']
449
self.context.url = unicode(data['url']) if data['url'] else None
450
self.context.worksheet_cutoff = data['worksheet_cutoff']
451
self.context.show_worksheet_marks = data['show_worksheet_marks']
455
class OfferingNew(BaseFormView):
456
"""A form to create an offering."""
457
template = 'templates/offering-new.html'
460
def authorize(self, req):
461
return req.user is not None and req.user.admin
465
return OfferingAdminSchema()
467
def populate(self, req, ctx):
468
super(OfferingNew, self).populate(req, ctx)
469
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
470
ctx['semesters'] = req.store.find(Semester).order_by(
471
Semester.year, Semester.semester)
472
ctx['force_subject'] = None
474
def populate_state(self, state):
475
state.existing_offering = None
477
def get_default_data(self, req):
480
def save_object(self, req, data):
481
new_offering = Offering()
482
new_offering.subject = data['subject']
483
new_offering.semester = data['semester']
484
new_offering.description = data['description']
485
new_offering.url = unicode(data['url']) if data['url'] else None
486
new_offering.worksheet_cutoff = data['worksheet_cutoff']
487
new_offering.show_worksheet_marks = data['show_worksheet_marks']
489
req.store.add(new_offering)
492
class SubjectOfferingNew(OfferingNew):
493
"""A form to create an offering for a given subject."""
494
# Identical to OfferingNew, except it forces the subject to be the subject
496
def populate(self, req, ctx):
497
super(SubjectOfferingNew, self).populate(req, ctx)
498
ctx['force_subject'] = self.context
500
class OfferingCloneWorksheetsSchema(formencode.Schema):
501
subject = formencode.All(
502
SubjectValidator(), formencode.validators.UnicodeString())
503
semester = formencode.All(
504
SemesterValidator(), formencode.validators.UnicodeString())
507
class OfferingCloneWorksheets(BaseFormView):
508
"""A form to clone worksheets from one offering to another."""
509
template = 'templates/offering-clone-worksheets.html'
512
def authorize(self, req):
513
return req.user is not None and req.user.admin
517
return OfferingCloneWorksheetsSchema()
519
def populate(self, req, ctx):
520
super(OfferingCloneWorksheets, self).populate(req, ctx)
521
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
522
ctx['semesters'] = req.store.find(Semester).order_by(
523
Semester.year, Semester.semester)
525
def get_default_data(self, req):
528
def save_object(self, req, data):
529
if self.context.worksheets.count() > 0:
531
"Cannot clone to target with existing worksheets.")
532
offering = req.store.find(
533
Offering, subject=data['subject'], semester=data['semester']).one()
535
raise BadRequest("No such offering.")
536
if offering.worksheets.count() == 0:
537
raise BadRequest("Source offering has no worksheets.")
539
self.context.clone_worksheets(offering)
61
enrolments = semester.enrolments.find(user=req.user)
62
if enrolments.count():
63
ctx['semesters'].append((semester, enrolments))
543
66
class UserValidator(formencode.FancyValidator):
632
131
ctx['data'] = data or {}
633
132
ctx['offering'] = self.context
634
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
635
133
ctx['errors'] = errors
636
# If all of the fields validated, set the global form error.
637
if isinstance(errors, basestring):
638
ctx['error_value'] = errors
641
class EnrolmentEditSchema(formencode.Schema):
642
role = formencode.All(formencode.validators.OneOf(
643
["lecturer", "tutor", "student"]),
644
RoleEnrolmentValidator(),
645
formencode.validators.UnicodeString())
648
class EnrolmentEdit(BaseFormView):
649
"""A form to alter an enrolment's role."""
650
template = 'templates/enrolment-edit.html'
654
def populate_state(self, state):
655
state.offering = self.context.offering
657
def get_default_data(self, req):
658
return {'role': self.context.role}
662
return EnrolmentEditSchema()
664
def save_object(self, req, data):
665
self.context.role = data['role']
667
def get_return_url(self, obj):
668
return self.req.publisher.generate(
669
self.context.offering, EnrolmentsView)
671
def populate(self, req, ctx):
672
super(EnrolmentEdit, self).populate(req, ctx)
673
ctx['offering_perms'] = self.context.offering.get_permissions(
674
req.user, req.config)
677
class EnrolmentDelete(XHTMLView):
678
"""A form to alter an enrolment's role."""
679
template = 'templates/enrolment-delete.html'
683
def populate(self, req, ctx):
684
# If POSTing, delete delete delete.
685
if req.method == 'POST':
686
self.context.delete()
688
req.throw_redirect(req.publisher.generate(
689
self.context.offering, EnrolmentsView))
691
ctx['enrolment'] = self.context
694
class OfferingProjectsView(XHTMLView):
695
"""View the projects for an offering."""
696
template = 'templates/offering_projects.html'
699
breadcrumb_text = 'Projects'
135
class SubjectProjectSetView(XHTMLView):
136
"""View the ProjectSets for a subject."""
137
template = 'templates/subject_projects.html'
140
def __init__(self, req, subject, year, semester):
141
self.context = req.store.find(Offering,
142
Offering.subject_id == Subject.id,
143
Subject.short_name == subject,
144
Offering.semester_id == Semester.id,
145
Semester.year == year,
146
Semester.semester == semester).one()
151
def project_url(self, projectset, project):
152
return "/subjects/" + self.context.subject.short_name + "/" +\
153
self.context.semester.year + "/" + \
154
self.context.semester.semester + "/+projectsets/" +\
155
str(projectset.id) + "/+projects/" + project.short_name
157
def new_project_url(self, projectset):
158
return "/api/subjects/" + self.context.subject.short_name + "/" +\
159
self.context.semester.year + "/" + \
160
self.context.semester.semester + "/+projectsets/" +\
161
str(projectset.id) + "/+projects/+new"
701
163
def populate(self, req, ctx):
702
164
self.plugin_styles[Plugin] = ["project.css"]
165
self.plugin_scripts[Plugin] = ["project.js"]
704
166
ctx['offering'] = self.context
167
ctx['subject'] = self.context.subject.short_name
168
ctx['year'] = self.context.semester.year
169
ctx['semester'] = self.context.semester.semester
705
171
ctx['projectsets'] = []
707
173
#Open the projectset Fragment, and render it for inclusion
708
174
#into the ProjectSets page
175
#XXX: This could be a lot cleaner
176
loader = genshi.template.TemplateLoader(".", auto_reload=True)
709
178
set_fragment = os.path.join(os.path.dirname(__file__),
710
179
"templates/projectset_fragment.html")
711
180
project_fragment = os.path.join(os.path.dirname(__file__),
712
181
"templates/project_fragment.html")
715
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
716
settmpl = self._loader.load(set_fragment)
183
for projectset in self.context.project_sets:
184
settmpl = loader.load(set_fragment)
717
185
setCtx = Context()
719
setCtx['projectset'] = projectset
186
setCtx['group_size'] = projectset.max_students_per_group
187
setCtx['projectset_id'] = projectset.id
188
setCtx['new_project_url'] = self.new_project_url(projectset)
720
189
setCtx['projects'] = []
721
setCtx['GroupsView'] = GroupsView
722
setCtx['ProjectSetEdit'] = ProjectSetEdit
723
setCtx['ProjectNew'] = ProjectNew
726
projectset.projects.order_by(ivle.database.Project.deadline):
727
projecttmpl = self._loader.load(project_fragment)
191
for project in projectset.projects:
192
projecttmpl = loader.load(project_fragment)
728
193
projectCtx = Context()
729
projectCtx['req'] = req
730
194
projectCtx['project'] = project
731
projectCtx['ProjectEdit'] = ProjectEdit
732
projectCtx['ProjectDelete'] = ProjectDelete
195
projectCtx['project_url'] = self.project_url(projectset, project)
734
197
setCtx['projects'].append(
735
198
projecttmpl.generate(projectCtx))
740
203
class ProjectView(XHTMLView):
741
204
"""View the submissions for a ProjectSet"""
742
205
template = "templates/project.html"
743
permission = "view_project_submissions"
746
def populate(self, req, ctx):
747
self.plugin_styles[Plugin] = ["project.css"]
750
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
751
ctx['GroupsView'] = GroupsView
752
ctx['EnrolView'] = EnrolView
753
ctx['format_datetime'] = ivle.date.make_date_nice
754
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
755
ctx['project'] = self.context
756
ctx['user'] = req.user
757
ctx['ProjectEdit'] = ProjectEdit
758
ctx['ProjectDelete'] = ProjectDelete
759
ctx['ProjectExport'] = ProjectBashExportView
761
class ProjectBashExportView(TextView):
762
"""Produce a Bash script for exporting projects"""
763
template = "templates/project-export.sh"
764
content_type = "text/x-sh"
765
permission = "view_project_submissions"
767
def populate(self, req, ctx):
769
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
770
ctx['format_datetime'] = ivle.date.make_date_nice
771
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
772
ctx['project'] = self.context
773
ctx['user'] = req.user
774
ctx['now'] = datetime.datetime.now()
775
ctx['format_datetime'] = ivle.date.make_date_nice
776
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
778
class ProjectUniquenessValidator(formencode.FancyValidator):
779
"""A FormEncode validator that checks that a project short_name is unique
782
The project referenced by state.existing_project is permitted to
783
hold that short_name. If any other project holds it, the input is rejected.
785
def _to_python(self, value, state):
786
if (state.store.find(
788
Project.short_name == unicode(value),
789
Project.project_set_id == ProjectSet.id,
790
ProjectSet.offering == state.offering).one() not in
791
(None, state.existing_project)):
792
raise formencode.Invalid(
793
"A project with that URL name already exists in this offering."
797
class ProjectSchema(formencode.Schema):
798
name = formencode.validators.UnicodeString(not_empty=True)
799
short_name = formencode.All(
800
URLNameValidator(not_empty=True),
801
ProjectUniquenessValidator())
802
deadline = DateTimeValidator(not_empty=True)
803
url = formencode.validators.URL(if_missing=None, not_empty=False)
804
synopsis = formencode.validators.UnicodeString(not_empty=True)
806
class ProjectEdit(BaseFormView):
807
"""A form to edit a project."""
808
template = 'templates/project-edit.html'
814
return ProjectSchema()
816
def populate(self, req, ctx):
817
super(ProjectEdit, self).populate(req, ctx)
818
ctx['projectset'] = self.context.project_set
820
def populate_state(self, state):
821
state.offering = self.context.project_set.offering
822
state.existing_project = self.context
824
def get_default_data(self, req):
826
'name': self.context.name,
827
'short_name': self.context.short_name,
828
'deadline': self.context.deadline,
829
'url': self.context.url,
830
'synopsis': self.context.synopsis,
833
def save_object(self, req, data):
834
self.context.name = data['name']
835
self.context.short_name = data['short_name']
836
self.context.deadline = data['deadline']
837
self.context.url = unicode(data['url']) if data['url'] else None
838
self.context.synopsis = data['synopsis']
841
class ProjectNew(BaseFormView):
842
"""A form to create a new project."""
843
template = 'templates/project-new.html'
849
return ProjectSchema()
851
def populate(self, req, ctx):
852
super(ProjectNew, self).populate(req, ctx)
853
ctx['projectset'] = self.context
855
def populate_state(self, state):
856
state.offering = self.context.offering
857
state.existing_project = None
859
def get_default_data(self, req):
862
def save_object(self, req, data):
863
new_project = Project()
864
new_project.project_set = self.context
865
new_project.name = data['name']
866
new_project.short_name = data['short_name']
867
new_project.deadline = data['deadline']
868
new_project.url = unicode(data['url']) if data['url'] else None
869
new_project.synopsis = data['synopsis']
870
req.store.add(new_project)
873
class ProjectDelete(XHTMLView):
874
"""A form to delete a project."""
875
template = 'templates/project-delete.html'
879
def populate(self, req, ctx):
880
# If post, delete the project, or display a message explaining that
881
# the project cannot be deleted
882
if self.context.can_delete:
883
if req.method == 'POST':
884
self.context.delete()
885
self.template = 'templates/project-deleted.html'
888
self.template = 'templates/project-undeletable.html'
890
# If get and can delete, display a delete confirmation page
892
# Variables for the template
894
ctx['project'] = self.context
895
ctx['OfferingProjectsView'] = OfferingProjectsView
897
class ProjectSetSchema(formencode.Schema):
898
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
900
class ProjectSetEdit(BaseFormView):
901
"""A form to edit a project set."""
902
template = 'templates/projectset-edit.html'
908
return ProjectSetSchema()
910
def populate(self, req, ctx):
911
super(ProjectSetEdit, self).populate(req, ctx)
913
def get_default_data(self, req):
915
'group_size': self.context.max_students_per_group,
918
def save_object(self, req, data):
919
self.context.max_students_per_group = data['group_size']
922
class ProjectSetNew(BaseFormView):
923
"""A form to create a new project set."""
924
template = 'templates/projectset-new.html'
927
breadcrumb_text = "Projects"
931
return ProjectSetSchema()
933
def populate(self, req, ctx):
934
super(ProjectSetNew, self).populate(req, ctx)
936
def get_default_data(self, req):
939
def save_object(self, req, data):
940
new_set = ProjectSet()
941
new_set.offering = self.context
942
new_set.max_students_per_group = data['group_size']
943
req.store.add(new_set)
208
def __init__(self, req, subject, year, semester, projectset, project):
209
self.context = req.store.find(Project,
210
Project.short_name == project,
211
Project.project_set_id == ProjectSet.id,
212
ProjectSet.offering_id == Offering.id,
213
Offering.semester_id == Semester.id,
214
Semester.year == year,
215
Semester.semester == semester,
216
Offering.subject_id == Subject.id,
217
Subject.short_name == subject).one()
218
if self.context is None:
221
def populate(self, req, ctx):
222
ctx['project'] = self.context
223
ctx['assesseds'] = self.context.assesseds
225
ctx['submissions'] = []
226
for assessed in self.context.assesseds:
227
if assessed.submissions.count() > 0:
228
ctx['submissions'].append(
229
assessed.submissions.order_by(ProjectSubmission.date_submitted)[:-1])
946
232
class Plugin(ViewPlugin, MediaPlugin):
947
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
948
offering_to_project, offering_to_projectset,
949
offering_to_enrolment)
951
subject_url, semester_url, offering_url, projectset_url, project_url,
954
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
955
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
956
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
957
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
958
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
959
(Subject, '+index', SubjectView),
960
(Subject, '+edit', SubjectEdit),
961
(Subject, '+new-offering', SubjectOfferingNew),
962
(Semester, '+edit', SemesterEdit),
963
(Offering, '+index', OfferingView),
964
(Offering, '+edit', OfferingEdit),
965
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
966
(Offering, ('+enrolments', '+index'), EnrolmentsView),
967
(Offering, ('+enrolments', '+new'), EnrolView),
968
(Enrolment, '+edit', EnrolmentEdit),
969
(Enrolment, '+delete', EnrolmentDelete),
970
(Offering, ('+projects', '+index'), OfferingProjectsView),
971
(Offering, ('+projects', '+new-set'), ProjectSetNew),
972
(ProjectSet, '+edit', ProjectSetEdit),
973
(ProjectSet, '+new', ProjectNew),
974
(Project, '+index', ProjectView),
975
(Project, '+edit', ProjectEdit),
976
(Project, '+delete', ProjectDelete),
977
(Project, ('+export', 'project-export.sh'),
978
ProjectBashExportView),
981
breadcrumbs = {Subject: SubjectBreadcrumb,
982
Offering: OfferingBreadcrumb,
983
User: UserBreadcrumb,
984
Project: ProjectBreadcrumb,
985
Enrolment: EnrolmentBreadcrumb,
234
('subjects/', SubjectsView),
235
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
236
('subjects/:subject/:year/:semester/+projects', SubjectProjectSetView),
237
('subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/:project', ProjectView),
239
('api/subjects/:subject/:year/:semester/+projectsets/+new',
241
('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
243
('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/:project',
989
249
('subjects', 'Subjects',