32
from storm.locals import Desc, Store
31
from storm.locals import Desc
34
33
from genshi.filters import HTMLFormFiller
35
from genshi.template import Context
34
from genshi.template import Context, TemplateLoader
37
import formencode.validators
39
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
37
from ivle.webapp.base.xhtml import XHTMLView
41
38
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
39
from ivle.webapp.errors import NotFound
47
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
48
42
ProjectSet, Project, ProjectSubmission
49
43
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
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
64
51
class SubjectsView(XHTMLView):
65
52
'''The view of the list of subjects.'''
66
53
template = 'templates/subjects.html'
68
breadcrumb_text = "Subjects"
70
56
def authorize(self, req):
71
57
return req.user is not None
73
59
def populate(self, req, ctx):
75
60
ctx['user'] = req.user
76
61
ctx['semesters'] = []
78
62
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
79
63
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 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'], semester=value['semester']
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
semester = URLNameValidator()
207
state = formencode.All(
208
formencode.validators.OneOf(["past", "current", "future"]),
209
formencode.validators.UnicodeString())
210
chained_validators = [SemesterUniquenessValidator()]
213
class SemesterFormView(BaseFormView):
216
def authorize(self, req):
217
return req.user is not None and req.user.admin
221
return SemesterSchema()
223
def get_return_url(self, obj):
224
return '/subjects/+manage'
227
class SemesterNew(SemesterFormView):
228
"""A form to create a semester."""
229
template = 'templates/semester-new.html'
232
def populate_state(self, state):
233
state.existing_semester = None
235
def get_default_data(self, req):
238
def save_object(self, req, data):
239
new_semester = Semester()
240
new_semester.year = data['year']
241
new_semester.semester = data['semester']
242
new_semester.state = data['state']
244
req.store.add(new_semester)
248
class SemesterEdit(SemesterFormView):
249
"""A form to edit a semester."""
250
template = 'templates/semester-edit.html'
252
def populate_state(self, state):
253
state.existing_semester = self.context
255
def get_default_data(self, req):
257
'year': self.context.year,
258
'semester': self.context.semester,
259
'state': self.context.state,
262
def save_object(self, req, data):
263
self.context.year = data['year']
264
self.context.semester = data['semester']
265
self.context.state = data['state']
269
class SubjectView(XHTMLView):
270
'''The view of the list of offerings in a given subject.'''
271
template = 'templates/subject.html'
274
def authorize(self, req):
275
return req.user is not None
277
def populate(self, req, ctx):
278
ctx['context'] = self.context
280
ctx['user'] = req.user
281
ctx['offerings'] = list(self.context.offerings)
282
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
283
ctx['SubjectEdit'] = SubjectEdit
284
ctx['SubjectOfferingNew'] = SubjectOfferingNew
287
class OfferingView(XHTMLView):
288
"""The home page of an offering."""
289
template = 'templates/offering.html'
293
def populate(self, req, ctx):
294
# Need the worksheet result styles.
295
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
296
ctx['context'] = self.context
298
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
299
ctx['format_submission_principal'] = util.format_submission_principal
300
ctx['format_datetime'] = ivle.date.make_date_nice
301
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
302
ctx['OfferingEdit'] = OfferingEdit
303
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
304
ctx['GroupsView'] = GroupsView
305
ctx['EnrolmentsView'] = EnrolmentsView
306
ctx['Project'] = ivle.database.Project
308
# As we go, calculate the total score for this subject
309
# (Assessable worksheets only, mandatory problems only)
311
ctx['worksheets'], problems_total, problems_done = (
312
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
313
req.config, req.store, req.user, self.context,
314
as_of=self.context.worksheet_cutoff))
316
ctx['exercises_total'] = problems_total
317
ctx['exercises_done'] = problems_done
318
if problems_total > 0:
319
if problems_done >= problems_total:
320
ctx['worksheets_complete_class'] = "complete"
321
elif problems_done > 0:
322
ctx['worksheets_complete_class'] = "semicomplete"
324
ctx['worksheets_complete_class'] = "incomplete"
325
# Calculate the final percentage and mark for the subject
326
(ctx['exercises_pct'], ctx['worksheet_mark'],
327
ctx['worksheet_max_mark']) = (
328
ivle.worksheet.utils.calculate_mark(
329
problems_done, problems_total))
332
class SubjectValidator(formencode.FancyValidator):
333
"""A FormEncode validator that turns a subject name into a subject.
335
The state must have a 'store' attribute, which is the Storm store
338
def _to_python(self, value, state):
339
subject = state.store.find(Subject, short_name=value).one()
343
raise formencode.Invalid('Subject does not exist', value, state)
346
class SemesterValidator(formencode.FancyValidator):
347
"""A FormEncode validator that turns a string into a semester.
349
The string should be of the form 'year/semester', eg. '2009/1'.
351
The state must have a 'store' attribute, which is the Storm store
354
def _to_python(self, value, state):
356
year, semester = value.split('/')
358
year = semester = None
360
semester = state.store.find(
361
Semester, year=year, semester=semester).one()
365
raise formencode.Invalid('Semester does not exist', value, state)
368
class OfferingUniquenessValidator(formencode.FancyValidator):
369
"""A FormEncode validator that checks that an offering is unique.
371
There cannot be more than one offering in the same year and semester.
373
The offering referenced by state.existing_offering is permitted to
374
hold that year and semester tuple. If any other object holds it, the
377
def _to_python(self, value, state):
378
if (state.store.find(
379
Offering, subject=value['subject'],
380
semester=value['semester']).one() not in
381
(None, state.existing_offering)):
382
raise formencode.Invalid(
383
'Offering already exists', value, state)
387
class OfferingSchema(formencode.Schema):
388
description = formencode.validators.UnicodeString(
389
if_missing=None, not_empty=False)
390
url = formencode.validators.URL(if_missing=None, not_empty=False)
391
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
392
show_worksheet_marks = formencode.validators.StringBoolean(
396
class OfferingAdminSchema(OfferingSchema):
397
subject = formencode.All(
398
SubjectValidator(), formencode.validators.UnicodeString())
399
semester = formencode.All(
400
SemesterValidator(), formencode.validators.UnicodeString())
401
chained_validators = [OfferingUniquenessValidator()]
404
class OfferingEdit(BaseFormView):
405
"""A form to edit an offering's details."""
406
template = 'templates/offering-edit.html'
412
if self.req.user.admin:
413
return OfferingAdminSchema()
415
return OfferingSchema()
417
def populate(self, req, ctx):
418
super(OfferingEdit, self).populate(req, ctx)
419
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
420
ctx['semesters'] = req.store.find(Semester).order_by(
421
Semester.year, Semester.semester)
422
ctx['force_subject'] = None
424
def populate_state(self, state):
425
state.existing_offering = self.context
427
def get_default_data(self, req):
429
'subject': self.context.subject.short_name,
430
'semester': self.context.semester.year + '/' +
431
self.context.semester.semester,
432
'url': self.context.url,
433
'description': self.context.description,
434
'worksheet_cutoff': self.context.worksheet_cutoff,
435
'show_worksheet_marks': self.context.show_worksheet_marks,
438
def save_object(self, req, data):
440
self.context.subject = data['subject']
441
self.context.semester = data['semester']
442
self.context.description = data['description']
443
self.context.url = unicode(data['url']) if data['url'] else None
444
self.context.worksheet_cutoff = data['worksheet_cutoff']
445
self.context.show_worksheet_marks = data['show_worksheet_marks']
449
class OfferingNew(BaseFormView):
450
"""A form to create an offering."""
451
template = 'templates/offering-new.html'
454
def authorize(self, req):
455
return req.user is not None and req.user.admin
459
return OfferingAdminSchema()
461
def populate(self, req, ctx):
462
super(OfferingNew, self).populate(req, ctx)
463
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
464
ctx['semesters'] = req.store.find(Semester).order_by(
465
Semester.year, Semester.semester)
466
ctx['force_subject'] = None
468
def populate_state(self, state):
469
state.existing_offering = None
471
def get_default_data(self, req):
474
def save_object(self, req, data):
475
new_offering = Offering()
476
new_offering.subject = data['subject']
477
new_offering.semester = data['semester']
478
new_offering.description = data['description']
479
new_offering.url = unicode(data['url']) if data['url'] else None
480
new_offering.worksheet_cutoff = data['worksheet_cutoff']
481
new_offering.show_worksheet_marks = data['show_worksheet_marks']
483
req.store.add(new_offering)
486
class SubjectOfferingNew(OfferingNew):
487
"""A form to create an offering for a given subject."""
488
# Identical to OfferingNew, except it forces the subject to be the subject
490
def populate(self, req, ctx):
491
super(SubjectOfferingNew, self).populate(req, ctx)
492
ctx['force_subject'] = self.context
494
class OfferingCloneWorksheetsSchema(formencode.Schema):
495
subject = formencode.All(
496
SubjectValidator(), formencode.validators.UnicodeString())
497
semester = formencode.All(
498
SemesterValidator(), formencode.validators.UnicodeString())
501
class OfferingCloneWorksheets(BaseFormView):
502
"""A form to clone worksheets from one offering to another."""
503
template = 'templates/offering-clone-worksheets.html'
506
def authorize(self, req):
507
return req.user is not None and req.user.admin
511
return OfferingCloneWorksheetsSchema()
513
def populate(self, req, ctx):
514
super(OfferingCloneWorksheets, self).populate(req, ctx)
515
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
516
ctx['semesters'] = req.store.find(Semester).order_by(
517
Semester.year, Semester.semester)
519
def get_default_data(self, req):
522
def save_object(self, req, data):
523
if self.context.worksheets.count() > 0:
525
"Cannot clone to target with existing worksheets.")
526
offering = req.store.find(
527
Offering, subject=data['subject'], semester=data['semester']).one()
529
raise BadRequest("No such offering.")
530
if offering.worksheets.count() == 0:
531
raise BadRequest("Source offering has no worksheets.")
533
self.context.clone_worksheets(offering)
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
537
69
class UserValidator(formencode.FancyValidator):
626
134
ctx['data'] = data or {}
627
135
ctx['offering'] = self.context
628
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
629
136
ctx['errors'] = errors
630
# If all of the fields validated, set the global form error.
631
if isinstance(errors, basestring):
632
ctx['error_value'] = errors
635
class EnrolmentEditSchema(formencode.Schema):
636
role = formencode.All(formencode.validators.OneOf(
637
["lecturer", "tutor", "student"]),
638
RoleEnrolmentValidator(),
639
formencode.validators.UnicodeString())
642
class EnrolmentEdit(BaseFormView):
643
"""A form to alter an enrolment's role."""
644
template = 'templates/enrolment-edit.html'
648
def populate_state(self, state):
649
state.offering = self.context.offering
651
def get_default_data(self, req):
652
return {'role': self.context.role}
656
return EnrolmentEditSchema()
658
def save_object(self, req, data):
659
self.context.role = data['role']
661
def get_return_url(self, obj):
662
return self.req.publisher.generate(
663
self.context.offering, EnrolmentsView)
665
def populate(self, req, ctx):
666
super(EnrolmentEdit, self).populate(req, ctx)
667
ctx['offering_perms'] = self.context.offering.get_permissions(
668
req.user, req.config)
671
class EnrolmentDelete(XHTMLView):
672
"""A form to alter an enrolment's role."""
673
template = 'templates/enrolment-delete.html'
677
def populate(self, req, ctx):
678
# If POSTing, delete delete delete.
679
if req.method == 'POST':
680
self.context.delete()
682
req.throw_redirect(req.publisher.generate(
683
self.context.offering, EnrolmentsView))
685
ctx['enrolment'] = self.context
688
138
class OfferingProjectsView(XHTMLView):
689
139
"""View the projects for an offering."""
690
140
template = 'templates/offering_projects.html'
691
141
permission = 'edit'
693
breadcrumb_text = 'Projects'
144
def __init__(self, req, subject, year, semester):
145
self.context = req.store.find(Offering,
146
Offering.subject_id == Subject.id,
147
Subject.short_name == subject,
148
Offering.semester_id == Semester.id,
149
Semester.year == year,
150
Semester.semester == semester).one()
155
def project_url(self, projectset, project):
156
return "/subjects/%s/%s/%s/+projects/%s" % (
157
self.context.subject.short_name,
158
self.context.semester.year,
159
self.context.semester.semester,
163
def new_project_url(self, projectset):
164
return "/api/subjects/" + self.context.subject.short_name + "/" +\
165
self.context.semester.year + "/" + \
166
self.context.semester.semester + "/+projectsets/" +\
167
str(projectset.id) + "/+projects/+new"
695
169
def populate(self, req, ctx):
696
170
self.plugin_styles[Plugin] = ["project.css"]
171
self.plugin_scripts[Plugin] = ["project.js"]
698
172
ctx['offering'] = self.context
699
173
ctx['projectsets'] = []
701
175
#Open the projectset Fragment, and render it for inclusion
702
176
#into the ProjectSets page
177
#XXX: This could be a lot cleaner
178
loader = genshi.template.TemplateLoader(".", auto_reload=True)
703
180
set_fragment = os.path.join(os.path.dirname(__file__),
704
181
"templates/projectset_fragment.html")
705
182
project_fragment = os.path.join(os.path.dirname(__file__),
706
183
"templates/project_fragment.html")
709
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
710
settmpl = self._loader.load(set_fragment)
185
for projectset in self.context.project_sets:
186
settmpl = loader.load(set_fragment)
711
187
setCtx = Context()
713
188
setCtx['projectset'] = projectset
189
setCtx['new_project_url'] = self.new_project_url(projectset)
714
190
setCtx['projects'] = []
715
setCtx['GroupsView'] = GroupsView
716
setCtx['ProjectSetEdit'] = ProjectSetEdit
717
setCtx['ProjectNew'] = ProjectNew
720
projectset.projects.order_by(ivle.database.Project.deadline):
721
projecttmpl = self._loader.load(project_fragment)
192
for project in projectset.projects:
193
projecttmpl = loader.load(project_fragment)
722
194
projectCtx = Context()
723
projectCtx['req'] = req
724
195
projectCtx['project'] = project
725
projectCtx['ProjectEdit'] = ProjectEdit
726
projectCtx['ProjectDelete'] = ProjectDelete
196
projectCtx['project_url'] = self.project_url(projectset, project)
728
198
setCtx['projects'].append(
729
199
projecttmpl.generate(projectCtx))
734
204
class ProjectView(XHTMLView):
735
205
"""View the submissions for a ProjectSet"""
736
206
template = "templates/project.html"
737
permission = "view_project_submissions"
210
def __init__(self, req, subject, year, semester, project):
211
self.context = req.store.find(Project,
212
Project.short_name == project,
213
Project.project_set_id == ProjectSet.id,
214
ProjectSet.offering_id == Offering.id,
215
Offering.semester_id == Semester.id,
216
Semester.year == year,
217
Semester.semester == semester,
218
Offering.subject_id == Subject.id,
219
Subject.short_name == subject).one()
220
if self.context is None:
223
def build_subversion_url(self, svnroot, submission):
224
princ = submission.assessed.principal
226
if isinstance(princ, User):
227
path = 'users/%s' % princ.login
229
path = 'groups/%s_%s_%s_%s' % (
230
princ.project_set.offering.subject.short_name,
231
princ.project_set.offering.semester.year,
232
princ.project_set.offering.semester.semester,
235
return urlparse.urljoin(
237
os.path.join(path, submission.path[1:] if
238
submission.path.startswith(os.sep) else
740
241
def populate(self, req, ctx):
741
242
self.plugin_styles[Plugin] = ["project.css"]
744
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
745
ctx['GroupsView'] = GroupsView
746
ctx['EnrolView'] = EnrolView
747
ctx['format_datetime'] = ivle.date.make_date_nice
748
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
749
ctx['project'] = self.context
750
ctx['user'] = req.user
751
ctx['ProjectEdit'] = ProjectEdit
752
ctx['ProjectDelete'] = ProjectDelete
753
ctx['ProjectExport'] = ProjectBashExportView
755
class ProjectBashExportView(TextView):
756
"""Produce a Bash script for exporting projects"""
757
template = "templates/project-export.sh"
758
content_type = "text/x-sh"
759
permission = "view_project_submissions"
761
def populate(self, req, ctx):
763
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
764
ctx['format_datetime'] = ivle.date.make_date_nice
765
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
766
ctx['project'] = self.context
767
ctx['user'] = req.user
768
ctx['now'] = datetime.datetime.now()
769
ctx['format_datetime'] = ivle.date.make_date_nice
770
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
772
class ProjectUniquenessValidator(formencode.FancyValidator):
773
"""A FormEncode validator that checks that a project short_name is unique
776
The project referenced by state.existing_project is permitted to
777
hold that short_name. If any other project holds it, the input is rejected.
779
def _to_python(self, value, state):
780
if (state.store.find(
782
Project.short_name == unicode(value),
783
Project.project_set_id == ProjectSet.id,
784
ProjectSet.offering == state.offering).one() not in
785
(None, state.existing_project)):
786
raise formencode.Invalid(
787
"A project with that URL name already exists in this offering."
791
class ProjectSchema(formencode.Schema):
792
name = formencode.validators.UnicodeString(not_empty=True)
793
short_name = formencode.All(
794
URLNameValidator(not_empty=True),
795
ProjectUniquenessValidator())
796
deadline = DateTimeValidator(not_empty=True)
797
url = formencode.validators.URL(if_missing=None, not_empty=False)
798
synopsis = formencode.validators.UnicodeString(not_empty=True)
800
class ProjectEdit(BaseFormView):
801
"""A form to edit a project."""
802
template = 'templates/project-edit.html'
808
return ProjectSchema()
810
def populate(self, req, ctx):
811
super(ProjectEdit, self).populate(req, ctx)
812
ctx['projectset'] = self.context.project_set
814
def populate_state(self, state):
815
state.offering = self.context.project_set.offering
816
state.existing_project = self.context
818
def get_default_data(self, req):
820
'name': self.context.name,
821
'short_name': self.context.short_name,
822
'deadline': self.context.deadline,
823
'url': self.context.url,
824
'synopsis': self.context.synopsis,
827
def save_object(self, req, data):
828
self.context.name = data['name']
829
self.context.short_name = data['short_name']
830
self.context.deadline = data['deadline']
831
self.context.url = unicode(data['url']) if data['url'] else None
832
self.context.synopsis = data['synopsis']
835
class ProjectNew(BaseFormView):
836
"""A form to create a new project."""
837
template = 'templates/project-new.html'
843
return ProjectSchema()
845
def populate(self, req, ctx):
846
super(ProjectNew, self).populate(req, ctx)
847
ctx['projectset'] = self.context
849
def populate_state(self, state):
850
state.offering = self.context.offering
851
state.existing_project = None
853
def get_default_data(self, req):
856
def save_object(self, req, data):
857
new_project = Project()
858
new_project.project_set = self.context
859
new_project.name = data['name']
860
new_project.short_name = data['short_name']
861
new_project.deadline = data['deadline']
862
new_project.url = unicode(data['url']) if data['url'] else None
863
new_project.synopsis = data['synopsis']
864
req.store.add(new_project)
867
class ProjectDelete(XHTMLView):
868
"""A form to delete a project."""
869
template = 'templates/project-delete.html'
873
def populate(self, req, ctx):
874
# If post, delete the project, or display a message explaining that
875
# the project cannot be deleted
876
if self.context.can_delete:
877
if req.method == 'POST':
878
self.context.delete()
879
self.template = 'templates/project-deleted.html'
882
self.template = 'templates/project-undeletable.html'
884
# If get and can delete, display a delete confirmation page
886
# Variables for the template
888
ctx['project'] = self.context
889
ctx['OfferingProjectsView'] = OfferingProjectsView
891
class ProjectSetSchema(formencode.Schema):
892
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
894
class ProjectSetEdit(BaseFormView):
895
"""A form to edit a project set."""
896
template = 'templates/projectset-edit.html'
902
return ProjectSetSchema()
904
def populate(self, req, ctx):
905
super(ProjectSetEdit, self).populate(req, ctx)
907
def get_default_data(self, req):
909
'group_size': self.context.max_students_per_group,
912
def save_object(self, req, data):
913
self.context.max_students_per_group = data['group_size']
916
class ProjectSetNew(BaseFormView):
917
"""A form to create a new project set."""
918
template = 'templates/projectset-new.html'
921
breadcrumb_text = "Projects"
925
return ProjectSetSchema()
927
def populate(self, req, ctx):
928
super(ProjectSetNew, self).populate(req, ctx)
930
def get_default_data(self, req):
933
def save_object(self, req, data):
934
new_set = ProjectSet()
935
new_set.offering = self.context
936
new_set.max_students_per_group = data['group_size']
937
req.store.add(new_set)
244
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
245
ctx['build_subversion_url'] = self.build_subversion_url
246
ctx['svn_addr'] = req.config['urls']['svn_addr']
247
ctx['project'] = self.context
248
ctx['user'] = req.user
940
250
class Plugin(ViewPlugin, MediaPlugin):
941
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
942
offering_to_project, offering_to_projectset,
943
offering_to_enrolment)
945
subject_url, semester_url, offering_url, projectset_url, project_url,
948
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
949
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
950
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
951
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
952
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
953
(Subject, '+index', SubjectView),
954
(Subject, '+edit', SubjectEdit),
955
(Subject, '+new-offering', SubjectOfferingNew),
956
(Semester, '+edit', SemesterEdit),
957
(Offering, '+index', OfferingView),
958
(Offering, '+edit', OfferingEdit),
959
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
960
(Offering, ('+enrolments', '+index'), EnrolmentsView),
961
(Offering, ('+enrolments', '+new'), EnrolView),
962
(Enrolment, '+edit', EnrolmentEdit),
963
(Enrolment, '+delete', EnrolmentDelete),
964
(Offering, ('+projects', '+index'), OfferingProjectsView),
965
(Offering, ('+projects', '+new-set'), ProjectSetNew),
966
(ProjectSet, '+edit', ProjectSetEdit),
967
(ProjectSet, '+new', ProjectNew),
968
(Project, '+index', ProjectView),
969
(Project, '+edit', ProjectEdit),
970
(Project, '+delete', ProjectDelete),
971
(Project, ('+export', 'project-export.sh'),
972
ProjectBashExportView),
975
breadcrumbs = {Subject: SubjectBreadcrumb,
976
Offering: OfferingBreadcrumb,
977
User: UserBreadcrumb,
978
Project: ProjectBreadcrumb,
979
Enrolment: EnrolmentBreadcrumb,
252
('subjects/', SubjectsView),
253
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
254
('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
255
('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
257
('api/subjects/:subject/:year/:semester/+projectsets/+new',
259
('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
261
('api/subjects/:subject/:year/:semester/+projects/:project',
983
267
('subjects', 'Subjects',