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 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)
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
543
69
class UserValidator(formencode.FancyValidator):
632
134
ctx['data'] = data or {}
633
135
ctx['offering'] = self.context
634
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
635
136
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
138
class OfferingProjectsView(XHTMLView):
695
139
"""View the projects for an offering."""
696
140
template = 'templates/offering_projects.html'
697
141
permission = 'edit'
699
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"
701
169
def populate(self, req, ctx):
702
170
self.plugin_styles[Plugin] = ["project.css"]
171
self.plugin_scripts[Plugin] = ["project.js"]
704
172
ctx['offering'] = self.context
705
173
ctx['projectsets'] = []
707
175
#Open the projectset Fragment, and render it for inclusion
708
176
#into the ProjectSets page
177
#XXX: This could be a lot cleaner
178
loader = genshi.template.TemplateLoader(".", auto_reload=True)
709
180
set_fragment = os.path.join(os.path.dirname(__file__),
710
181
"templates/projectset_fragment.html")
711
182
project_fragment = os.path.join(os.path.dirname(__file__),
712
183
"templates/project_fragment.html")
715
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
716
settmpl = self._loader.load(set_fragment)
185
for projectset in self.context.project_sets:
186
settmpl = loader.load(set_fragment)
717
187
setCtx = Context()
719
188
setCtx['projectset'] = projectset
189
setCtx['new_project_url'] = self.new_project_url(projectset)
720
190
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)
192
for project in projectset.projects:
193
projecttmpl = loader.load(project_fragment)
728
194
projectCtx = Context()
729
projectCtx['req'] = req
730
195
projectCtx['project'] = project
731
projectCtx['ProjectEdit'] = ProjectEdit
732
projectCtx['ProjectDelete'] = ProjectDelete
196
projectCtx['project_url'] = self.project_url(projectset, project)
734
198
setCtx['projects'].append(
735
199
projecttmpl.generate(projectCtx))
740
204
class ProjectView(XHTMLView):
741
205
"""View the submissions for a ProjectSet"""
742
206
template = "templates/project.html"
743
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
746
241
def populate(self, req, ctx):
747
242
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)
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
946
250
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,
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',
989
267
('subjects', 'Subjects',