57
68
return req.user is not None
59
70
def populate(self, req, ctx):
60
72
ctx['user'] = req.user
61
73
ctx['semesters'] = []
62
75
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
63
76
Desc(Semester.semester)):
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
78
# For admins, show all subjects in the system
79
offerings = list(semester.offerings.find())
81
offerings = [enrolment.offering for enrolment in
82
semester.enrolments.find(user=req.user)]
84
ctx['semesters'].append((semester, offerings))
87
class SubjectsManage(XHTMLView):
88
'''Subject management view.'''
89
template = 'templates/subjects-manage.html'
92
def authorize(self, req):
93
return req.user is not None and req.user.admin
95
def populate(self, req, ctx):
97
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
98
ctx['SubjectEdit'] = SubjectEdit
99
ctx['SemesterEdit'] = SemesterEdit
101
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
102
ctx['semesters'] = req.store.find(Semester).order_by(
103
Semester.year, Semester.semester)
106
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
107
"""A FormEncode validator that checks that a subject name is unused.
109
The subject referenced by state.existing_subject is permitted
110
to hold that name. If any other object holds it, the input is rejected.
112
def __init__(self, matching=None):
113
self.matching = matching
115
def _to_python(self, value, state):
116
if (state.store.find(
117
Subject, short_name=value).one() not in
118
(None, state.existing_subject)):
119
raise formencode.Invalid(
120
'Short name already taken', value, state)
124
class SubjectSchema(formencode.Schema):
125
short_name = formencode.All(
126
SubjectShortNameUniquenessValidator(),
127
formencode.validators.UnicodeString(not_empty=True))
128
name = formencode.validators.UnicodeString(not_empty=True)
129
code = formencode.validators.UnicodeString(not_empty=True)
132
class SubjectFormView(BaseFormView):
133
"""An abstract form to add or edit a subject."""
136
def authorize(self, req):
137
return req.user is not None and req.user.admin
139
def populate_state(self, state):
140
state.existing_subject = None
144
return SubjectSchema()
146
def get_return_url(self, obj):
150
class SubjectNew(SubjectFormView):
151
"""A form to create a subject."""
152
template = 'templates/subject-new.html'
154
def get_default_data(self, req):
157
def save_object(self, req, data):
158
new_subject = Subject()
159
new_subject.short_name = data['short_name']
160
new_subject.name = data['name']
161
new_subject.code = data['code']
163
req.store.add(new_subject)
167
class SubjectEdit(SubjectFormView):
168
"""A form to edit a subject."""
169
template = 'templates/subject-edit.html'
171
def populate_state(self, state):
172
state.existing_subject = self.context
174
def get_default_data(self, req):
176
'short_name': self.context.short_name,
177
'name': self.context.name,
178
'code': self.context.code,
181
def save_object(self, req, data):
182
self.context.short_name = data['short_name']
183
self.context.name = data['name']
184
self.context.code = data['code']
189
class SemesterUniquenessValidator(formencode.FancyValidator):
190
"""A FormEncode validator that checks that a semester is unique.
192
There cannot be more than one semester for the same year and semester.
194
def _to_python(self, value, state):
195
if (state.store.find(
196
Semester, year=value['year'], semester=value['semester']
197
).one() not in (None, state.existing_semester)):
198
raise formencode.Invalid(
199
'Semester already exists', value, state)
203
class SemesterSchema(formencode.Schema):
204
year = formencode.validators.UnicodeString()
205
semester = formencode.validators.UnicodeString()
206
state = formencode.All(
207
formencode.validators.OneOf(["past", "current", "future"]),
208
formencode.validators.UnicodeString())
209
chained_validators = [SemesterUniquenessValidator()]
212
class SemesterFormView(BaseFormView):
215
def authorize(self, req):
216
return req.user is not None and req.user.admin
220
return SemesterSchema()
222
def get_return_url(self, obj):
223
return '/subjects/+manage'
226
class SemesterNew(SemesterFormView):
227
"""A form to create a semester."""
228
template = 'templates/semester-new.html'
231
def populate_state(self, state):
232
state.existing_semester = None
234
def get_default_data(self, req):
237
def save_object(self, req, data):
238
new_semester = Semester()
239
new_semester.year = data['year']
240
new_semester.semester = data['semester']
241
new_semester.state = data['state']
243
req.store.add(new_semester)
247
class SemesterEdit(SemesterFormView):
248
"""A form to edit a semester."""
249
template = 'templates/semester-edit.html'
251
def populate_state(self, state):
252
state.existing_semester = self.context
254
def get_default_data(self, req):
256
'year': self.context.year,
257
'semester': self.context.semester,
258
'state': self.context.state,
261
def save_object(self, req, data):
262
self.context.year = data['year']
263
self.context.semester = data['semester']
264
self.context.state = data['state']
269
class OfferingView(XHTMLView):
270
"""The home page of an offering."""
271
template = 'templates/offering.html'
275
def populate(self, req, ctx):
276
# Need the worksheet result styles.
277
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
278
ctx['context'] = self.context
280
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
281
ctx['format_submission_principal'] = util.format_submission_principal
282
ctx['format_datetime'] = ivle.date.make_date_nice
283
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
284
ctx['OfferingEdit'] = OfferingEdit
285
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
286
ctx['GroupsView'] = GroupsView
288
# As we go, calculate the total score for this subject
289
# (Assessable worksheets only, mandatory problems only)
291
ctx['worksheets'], problems_total, problems_done = (
292
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
293
req.store, req.user, self.context))
295
ctx['exercises_total'] = problems_total
296
ctx['exercises_done'] = problems_done
297
if problems_total > 0:
298
if problems_done >= problems_total:
299
ctx['worksheets_complete_class'] = "complete"
300
elif problems_done > 0:
301
ctx['worksheets_complete_class'] = "semicomplete"
303
ctx['worksheets_complete_class'] = "incomplete"
304
# Calculate the final percentage and mark for the subject
305
(ctx['exercises_pct'], ctx['worksheet_mark'],
306
ctx['worksheet_max_mark']) = (
307
ivle.worksheet.utils.calculate_mark(
308
problems_done, problems_total))
311
class SubjectValidator(formencode.FancyValidator):
312
"""A FormEncode validator that turns a subject name into a subject.
314
The state must have a 'store' attribute, which is the Storm store
317
def _to_python(self, value, state):
318
subject = state.store.find(Subject, short_name=value).one()
322
raise formencode.Invalid('Subject does not exist', value, state)
325
class SemesterValidator(formencode.FancyValidator):
326
"""A FormEncode validator that turns a string into a semester.
328
The string should be of the form 'year/semester', eg. '2009/1'.
330
The state must have a 'store' attribute, which is the Storm store
333
def _to_python(self, value, state):
335
year, semester = value.split('/')
337
year = semester = None
339
semester = state.store.find(
340
Semester, year=year, semester=semester).one()
344
raise formencode.Invalid('Semester does not exist', value, state)
347
class OfferingUniquenessValidator(formencode.FancyValidator):
348
"""A FormEncode validator that checks that an offering is unique.
350
There cannot be more than one offering in the same year and semester.
352
The offering referenced by state.existing_offering is permitted to
353
hold that year and semester tuple. If any other object holds it, the
356
def _to_python(self, value, state):
357
if (state.store.find(
358
Offering, subject=value['subject'],
359
semester=value['semester']).one() not in
360
(None, state.existing_offering)):
361
raise formencode.Invalid(
362
'Offering already exists', value, state)
366
class OfferingSchema(formencode.Schema):
367
description = formencode.validators.UnicodeString(
368
if_missing=None, not_empty=False)
369
url = formencode.validators.URL(if_missing=None, not_empty=False)
372
class OfferingAdminSchema(OfferingSchema):
373
subject = formencode.All(
374
SubjectValidator(), formencode.validators.UnicodeString())
375
semester = formencode.All(
376
SemesterValidator(), formencode.validators.UnicodeString())
377
chained_validators = [OfferingUniquenessValidator()]
380
class OfferingEdit(BaseFormView):
381
"""A form to edit an offering's details."""
382
template = 'templates/offering-edit.html'
388
if self.req.user.admin:
389
return OfferingAdminSchema()
391
return OfferingSchema()
393
def populate(self, req, ctx):
394
super(OfferingEdit, self).populate(req, ctx)
395
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
396
ctx['semesters'] = req.store.find(Semester).order_by(
397
Semester.year, Semester.semester)
399
def populate_state(self, state):
400
state.existing_offering = self.context
402
def get_default_data(self, req):
404
'subject': self.context.subject.short_name,
405
'semester': self.context.semester.year + '/' +
406
self.context.semester.semester,
407
'url': self.context.url,
408
'description': self.context.description,
411
def save_object(self, req, data):
413
self.context.subject = data['subject']
414
self.context.semester = data['semester']
415
self.context.description = data['description']
416
self.context.url = unicode(data['url']) if data['url'] else None
420
class OfferingNew(BaseFormView):
421
"""A form to create an offering."""
422
template = 'templates/offering-new.html'
425
def authorize(self, req):
426
return req.user is not None and req.user.admin
430
return OfferingAdminSchema()
432
def populate(self, req, ctx):
433
super(OfferingNew, self).populate(req, ctx)
434
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
435
ctx['semesters'] = req.store.find(Semester).order_by(
436
Semester.year, Semester.semester)
438
def populate_state(self, state):
439
state.existing_offering = None
441
def get_default_data(self, req):
444
def save_object(self, req, data):
445
new_offering = Offering()
446
new_offering.subject = data['subject']
447
new_offering.semester = data['semester']
448
new_offering.description = data['description']
449
new_offering.url = unicode(data['url']) if data['url'] else None
451
req.store.add(new_offering)
455
class OfferingCloneWorksheetsSchema(formencode.Schema):
456
subject = formencode.All(
457
SubjectValidator(), formencode.validators.UnicodeString())
458
semester = formencode.All(
459
SemesterValidator(), formencode.validators.UnicodeString())
462
class OfferingCloneWorksheets(BaseFormView):
463
"""A form to clone worksheets from one offering to another."""
464
template = 'templates/offering-clone-worksheets.html'
467
def authorize(self, req):
468
return req.user is not None and req.user.admin
472
return OfferingCloneWorksheetsSchema()
474
def populate(self, req, ctx):
475
super(OfferingCloneWorksheets, self).populate(req, ctx)
476
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
477
ctx['semesters'] = req.store.find(Semester).order_by(
478
Semester.year, Semester.semester)
480
def get_default_data(self, req):
483
def save_object(self, req, data):
484
if self.context.worksheets.count() > 0:
486
"Cannot clone to target with existing worksheets.")
487
offering = req.store.find(
488
Offering, subject=data['subject'], semester=data['semester']).one()
490
raise BadRequest("No such offering.")
491
if offering.worksheets.count() == 0:
492
raise BadRequest("Source offering has no worksheets.")
494
self.context.clone_worksheets(offering)
69
498
class UserValidator(formencode.FancyValidator):
248
662
ctx['user'] = req.user
250
664
class Plugin(ViewPlugin, MediaPlugin):
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',
665
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
666
offering_to_project, offering_to_projectset)
668
subject_url, semester_url, offering_url, projectset_url, project_url)
670
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
671
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
672
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
673
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
674
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
675
(Subject, '+edit', SubjectEdit),
676
(Semester, '+edit', SemesterEdit),
677
(Offering, '+index', OfferingView),
678
(Offering, '+edit', OfferingEdit),
679
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
680
(Offering, ('+enrolments', '+index'), EnrolmentsView),
681
(Offering, ('+enrolments', '+new'), EnrolView),
682
(Offering, ('+projects', '+index'), OfferingProjectsView),
683
(Project, '+index', ProjectView),
685
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
686
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
689
breadcrumbs = {Subject: SubjectBreadcrumb,
690
Offering: OfferingBreadcrumb,
691
User: UserBreadcrumb,
692
Project: ProjectBreadcrumb,
267
696
('subjects', 'Subjects',