57
66
return req.user is not None
59
68
def populate(self, req, ctx):
60
70
ctx['user'] = req.user
61
71
ctx['semesters'] = []
72
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
73
ctx['SubjectEdit'] = SubjectEdit
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))
86
# Admins get a separate list of subjects so they can add/edit.
88
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
91
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
92
"""A FormEncode validator that checks that a subject name is unused.
94
The subject referenced by state.existing_subject is permitted
95
to hold that name. If any other object holds it, the input is rejected.
97
def __init__(self, matching=None):
98
self.matching = matching
100
def _to_python(self, value, state):
101
if (state.store.find(
102
Subject, short_name=value).one() not in
103
(None, state.existing_subject)):
104
raise formencode.Invalid(
105
'Short name already taken', value, state)
109
class SubjectSchema(formencode.Schema):
110
short_name = formencode.All(
111
SubjectShortNameUniquenessValidator(),
112
formencode.validators.UnicodeString(not_empty=True))
113
name = formencode.validators.UnicodeString(not_empty=True)
114
code = formencode.validators.UnicodeString(not_empty=True)
117
class SubjectFormView(BaseFormView):
118
"""An abstract form to add or edit a subject."""
121
def authorize(self, req):
122
return req.user is not None and req.user.admin
124
def populate_state(self, state):
125
state.existing_subject = None
129
return SubjectSchema()
131
def get_return_url(self, obj):
135
class SubjectNew(SubjectFormView):
136
"""A form to create a subject."""
137
template = 'templates/subject-new.html'
139
def get_default_data(self, req):
142
def save_object(self, req, data):
143
new_subject = Subject()
144
new_subject.short_name = data['short_name']
145
new_subject.name = data['name']
146
new_subject.code = data['code']
148
req.store.add(new_subject)
152
class SubjectEdit(SubjectFormView):
153
"""A form to edit a subject."""
154
template = 'templates/subject-edit.html'
156
def populate_state(self, state):
157
state.existing_subject = self.context
159
def get_default_data(self, req):
161
'short_name': self.context.short_name,
162
'name': self.context.name,
163
'code': self.context.code,
166
def save_object(self, req, data):
167
self.context.short_name = data['short_name']
168
self.context.name = data['name']
169
self.context.code = data['code']
174
class SemesterUniquenessValidator(formencode.FancyValidator):
175
"""A FormEncode validator that checks that a semester is unique.
177
There cannot be more than one semester for the same year and semester.
179
def _to_python(self, value, state):
180
if (state.store.find(
181
Semester, year=value['year'], semester=value['semester']
183
raise formencode.Invalid(
184
'Semester already exists', value, state)
188
class SemesterSchema(formencode.Schema):
189
year = formencode.validators.UnicodeString()
190
semester = formencode.validators.UnicodeString()
191
chained_validators = [SemesterUniquenessValidator()]
194
class SemesterNew(BaseFormView):
195
"""A form to create a semester."""
196
template = 'templates/semester-new.html'
199
def authorize(self, req):
200
return req.user is not None and req.user.admin
204
return SemesterSchema()
206
def get_default_data(self, req):
209
def save_object(self, req, data):
210
new_semester = Semester()
211
new_semester.year = data['year']
212
new_semester.semester = data['semester']
214
req.store.add(new_semester)
217
def get_return_url(self, obj):
221
class OfferingView(XHTMLView):
222
"""The home page of an offering."""
223
template = 'templates/offering.html'
227
def populate(self, req, ctx):
228
# Need the worksheet result styles.
229
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
230
ctx['context'] = self.context
232
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
233
ctx['format_submission_principal'] = util.format_submission_principal
234
ctx['format_datetime'] = ivle.date.make_date_nice
235
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
236
ctx['OfferingEdit'] = OfferingEdit
237
ctx['GroupsView'] = GroupsView
239
# As we go, calculate the total score for this subject
240
# (Assessable worksheets only, mandatory problems only)
242
ctx['worksheets'], problems_total, problems_done = (
243
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
244
req.store, req.user, self.context))
246
ctx['exercises_total'] = problems_total
247
ctx['exercises_done'] = problems_done
248
if problems_total > 0:
249
if problems_done >= problems_total:
250
ctx['worksheets_complete_class'] = "complete"
251
elif problems_done > 0:
252
ctx['worksheets_complete_class'] = "semicomplete"
254
ctx['worksheets_complete_class'] = "incomplete"
255
# Calculate the final percentage and mark for the subject
256
(ctx['exercises_pct'], ctx['worksheet_mark'],
257
ctx['worksheet_max_mark']) = (
258
ivle.worksheet.utils.calculate_mark(
259
problems_done, problems_total))
262
class SubjectValidator(formencode.FancyValidator):
263
"""A FormEncode validator that turns a subject name into a subject.
265
The state must have a 'store' attribute, which is the Storm store
268
def _to_python(self, value, state):
269
subject = state.store.find(Subject, short_name=value).one()
273
raise formencode.Invalid('Subject does not exist', value, state)
276
class SemesterValidator(formencode.FancyValidator):
277
"""A FormEncode validator that turns a string into a semester.
279
The string should be of the form 'year/semester', eg. '2009/1'.
281
The state must have a 'store' attribute, which is the Storm store
284
def _to_python(self, value, state):
286
year, semester = value.split('/')
288
year = semester = None
290
semester = state.store.find(
291
Semester, year=year, semester=semester).one()
295
raise formencode.Invalid('Semester does not exist', value, state)
298
class OfferingUniquenessValidator(formencode.FancyValidator):
299
"""A FormEncode validator that checks that an offering is unique.
301
There cannot be more than one offering in the same year and semester.
303
The offering referenced by state.existing_offering is permitted to
304
hold that year and semester tuple. If any other object holds it, the
307
def _to_python(self, value, state):
308
if (state.store.find(
309
Offering, subject=value['subject'],
310
semester=value['semester']).one() not in
311
(None, state.existing_offering)):
312
raise formencode.Invalid(
313
'Offering already exists', value, state)
317
class OfferingSchema(formencode.Schema):
318
description = formencode.validators.UnicodeString(
319
if_missing=None, not_empty=False)
320
url = formencode.validators.URL(if_missing=None, not_empty=False)
323
class OfferingAdminSchema(OfferingSchema):
324
subject = formencode.All(
325
SubjectValidator(), formencode.validators.UnicodeString())
326
semester = formencode.All(
327
SemesterValidator(), formencode.validators.UnicodeString())
328
chained_validators = [OfferingUniquenessValidator()]
331
class OfferingEdit(BaseFormView):
332
"""A form to edit an offering's details."""
333
template = 'templates/offering-edit.html'
339
if self.req.user.admin:
340
return OfferingAdminSchema()
342
return OfferingSchema()
344
def populate(self, req, ctx):
345
super(OfferingEdit, self).populate(req, ctx)
346
ctx['subjects'] = req.store.find(Subject)
347
ctx['semesters'] = req.store.find(Semester)
349
def populate_state(self, state):
350
state.existing_offering = self.context
352
def get_default_data(self, req):
354
'subject': self.context.subject.short_name,
355
'semester': self.context.semester.year + '/' +
356
self.context.semester.semester,
357
'url': self.context.url,
358
'description': self.context.description,
361
def save_object(self, req, data):
363
self.context.subject = data['subject']
364
self.context.semester = data['semester']
365
self.context.description = data['description']
366
self.context.url = unicode(data['url']) if data['url'] else None
370
class OfferingNew(BaseFormView):
371
"""A form to create an offering."""
372
template = 'templates/offering-new.html'
375
def authorize(self, req):
376
return req.user is not None and req.user.admin
380
return OfferingAdminSchema()
382
def populate(self, req, ctx):
383
super(OfferingNew, self).populate(req, ctx)
384
ctx['subjects'] = req.store.find(Subject)
385
ctx['semesters'] = req.store.find(Semester)
387
def populate_state(self, state):
388
state.existing_offering = None
390
def get_default_data(self, req):
393
def save_object(self, req, data):
394
new_offering = Offering()
395
new_offering.subject = data['subject']
396
new_offering.semester = data['semester']
397
new_offering.description = data['description']
398
new_offering.url = unicode(data['url']) if data['url'] else None
400
req.store.add(new_offering)
69
404
class UserValidator(formencode.FancyValidator):
248
568
ctx['user'] = req.user
250
570
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',
571
forward_routes = (root_to_subject, subject_to_offering,
572
offering_to_project, offering_to_projectset)
573
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
575
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
576
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
577
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
578
(ApplicationRoot, ('subjects', '+new-semester'), SemesterNew),
579
(Subject, '+edit', SubjectEdit),
580
(Offering, '+index', OfferingView),
581
(Offering, '+edit', OfferingEdit),
582
(Offering, ('+enrolments', '+index'), EnrolmentsView),
583
(Offering, ('+enrolments', '+new'), EnrolView),
584
(Offering, ('+projects', '+index'), OfferingProjectsView),
585
(Project, '+index', ProjectView),
587
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
588
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
591
breadcrumbs = {Subject: SubjectBreadcrumb,
592
Offering: OfferingBreadcrumb,
593
User: UserBreadcrumb,
594
Project: ProjectBreadcrumb,
267
598
('subjects', 'Subjects',