108
125
class SubjectSchema(formencode.Schema):
109
126
short_name = formencode.All(
110
127
SubjectShortNameUniquenessValidator(),
111
formencode.validators.UnicodeString(not_empty=True))
128
URLNameValidator(not_empty=True))
112
129
name = formencode.validators.UnicodeString(not_empty=True)
113
130
code = formencode.validators.UnicodeString(not_empty=True)
116
class SubjectFormView(XHTMLView):
133
class SubjectFormView(BaseFormView):
117
134
"""An abstract form to add or edit a subject."""
120
137
def authorize(self, req):
121
138
return req.user is not None and req.user.admin
123
def filter(self, stream, ctx):
124
return stream | HTMLFormFiller(data=ctx['data'])
126
140
def populate_state(self, state):
127
141
state.existing_subject = None
129
def populate(self, req, ctx):
130
if req.method == 'POST':
131
data = dict(req.get_fieldstorage())
133
validator = SubjectSchema()
134
self.populate_state(req)
135
data = validator.to_python(data, state=req)
137
subject = self.update_subject_object(req, data)
140
req.throw_redirect(req.publisher.generate(subject))
141
except formencode.Invalid, e:
142
errors = e.unpack_errors()
144
data = self.get_default_data(req)
150
ctx['context'] = self.context
151
ctx['data'] = data or {}
152
ctx['errors'] = errors
145
return SubjectSchema()
147
def get_return_url(self, obj):
155
151
class SubjectNew(SubjectFormView):
156
152
"""A form to create a subject."""
157
153
template = 'templates/subject-new.html'
159
def populate_state(self, state):
160
state.existing_subject = self.context
162
155
def get_default_data(self, req):
165
def update_subject_object(self, req, data):
158
def save_object(self, req, data):
166
159
new_subject = Subject()
167
160
new_subject.short_name = data['short_name']
168
161
new_subject.name = data['name']
194
187
return self.context
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']
197
270
class OfferingView(XHTMLView):
198
271
"""The home page of an offering."""
199
272
template = 'templates/offering.html'
234
310
problems_done, problems_total))
313
class SubjectValidator(formencode.FancyValidator):
314
"""A FormEncode validator that turns a subject name into a subject.
316
The state must have a 'store' attribute, which is the Storm store
319
def _to_python(self, value, state):
320
subject = state.store.find(Subject, short_name=value).one()
324
raise formencode.Invalid('Subject does not exist', value, state)
327
class SemesterValidator(formencode.FancyValidator):
328
"""A FormEncode validator that turns a string into a semester.
330
The string should be of the form 'year/semester', eg. '2009/1'.
332
The state must have a 'store' attribute, which is the Storm store
335
def _to_python(self, value, state):
337
year, semester = value.split('/')
339
year = semester = None
341
semester = state.store.find(
342
Semester, year=year, semester=semester).one()
346
raise formencode.Invalid('Semester does not exist', value, state)
349
class OfferingUniquenessValidator(formencode.FancyValidator):
350
"""A FormEncode validator that checks that an offering is unique.
352
There cannot be more than one offering in the same year and semester.
354
The offering referenced by state.existing_offering is permitted to
355
hold that year and semester tuple. If any other object holds it, the
358
def _to_python(self, value, state):
359
if (state.store.find(
360
Offering, subject=value['subject'],
361
semester=value['semester']).one() not in
362
(None, state.existing_offering)):
363
raise formencode.Invalid(
364
'Offering already exists', value, state)
237
368
class OfferingSchema(formencode.Schema):
238
369
description = formencode.validators.UnicodeString(
239
370
if_missing=None, not_empty=False)
240
371
url = formencode.validators.URL(if_missing=None, not_empty=False)
243
class OfferingEdit(XHTMLView):
374
class OfferingAdminSchema(OfferingSchema):
375
subject = formencode.All(
376
SubjectValidator(), formencode.validators.UnicodeString())
377
semester = formencode.All(
378
SemesterValidator(), formencode.validators.UnicodeString())
379
chained_validators = [OfferingUniquenessValidator()]
382
class OfferingEdit(BaseFormView):
244
383
"""A form to edit an offering's details."""
245
384
template = 'templates/offering-edit.html'
247
386
permission = 'edit'
249
def filter(self, stream, ctx):
250
return stream | HTMLFormFiller(data=ctx['data'])
252
def populate(self, req, ctx):
253
if req.method == 'POST':
254
data = dict(req.get_fieldstorage())
256
validator = OfferingSchema()
257
data = validator.to_python(data, state=req)
259
self.context.url = unicode(data['url']) if data['url'] else None
260
self.context.description = data['description']
262
req.throw_redirect(req.publisher.generate(self.context))
263
except formencode.Invalid, e:
264
errors = e.unpack_errors()
390
if self.req.user.admin:
391
return OfferingAdminSchema()
267
'url': self.context.url,
268
'description': self.context.description,
393
return OfferingSchema()
395
def populate(self, req, ctx):
396
super(OfferingEdit, self).populate(req, ctx)
397
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
398
ctx['semesters'] = req.store.find(Semester).order_by(
399
Semester.year, Semester.semester)
401
def populate_state(self, state):
402
state.existing_offering = self.context
404
def get_default_data(self, req):
406
'subject': self.context.subject.short_name,
407
'semester': self.context.semester.year + '/' +
408
self.context.semester.semester,
409
'url': self.context.url,
410
'description': self.context.description,
272
ctx['data'] = data or {}
273
ctx['context'] = self.context
274
ctx['errors'] = errors
413
def save_object(self, req, data):
415
self.context.subject = data['subject']
416
self.context.semester = data['semester']
417
self.context.description = data['description']
418
self.context.url = unicode(data['url']) if data['url'] else None
422
class OfferingNew(BaseFormView):
423
"""A form to create an offering."""
424
template = 'templates/offering-new.html'
427
def authorize(self, req):
428
return req.user is not None and req.user.admin
432
return OfferingAdminSchema()
434
def populate(self, req, ctx):
435
super(OfferingNew, self).populate(req, ctx)
436
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
437
ctx['semesters'] = req.store.find(Semester).order_by(
438
Semester.year, Semester.semester)
440
def populate_state(self, state):
441
state.existing_offering = None
443
def get_default_data(self, req):
446
def save_object(self, req, data):
447
new_offering = Offering()
448
new_offering.subject = data['subject']
449
new_offering.semester = data['semester']
450
new_offering.description = data['description']
451
new_offering.url = unicode(data['url']) if data['url'] else None
453
req.store.add(new_offering)
457
class OfferingCloneWorksheetsSchema(formencode.Schema):
458
subject = formencode.All(
459
SubjectValidator(), formencode.validators.UnicodeString())
460
semester = formencode.All(
461
SemesterValidator(), formencode.validators.UnicodeString())
464
class OfferingCloneWorksheets(BaseFormView):
465
"""A form to clone worksheets from one offering to another."""
466
template = 'templates/offering-clone-worksheets.html'
469
def authorize(self, req):
470
return req.user is not None and req.user.admin
474
return OfferingCloneWorksheetsSchema()
476
def populate(self, req, ctx):
477
super(OfferingCloneWorksheets, self).populate(req, ctx)
478
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
479
ctx['semesters'] = req.store.find(Semester).order_by(
480
Semester.year, Semester.semester)
482
def get_default_data(self, req):
485
def save_object(self, req, data):
486
if self.context.worksheets.count() > 0:
488
"Cannot clone to target with existing worksheets.")
489
offering = req.store.find(
490
Offering, subject=data['subject'], semester=data['semester']).one()
492
raise BadRequest("No such offering.")
493
if offering.worksheets.count() == 0:
494
raise BadRequest("Source offering has no worksheets.")
496
self.context.clone_worksheets(offering)
277
500
class UserValidator(formencode.FancyValidator):
356
589
ctx['data'] = data or {}
357
590
ctx['offering'] = self.context
358
ctx['roles_auth'] = self.context.get_permissions(req.user)
591
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
359
592
ctx['errors'] = errors
595
class EnrolmentEditSchema(formencode.Schema):
596
role = formencode.All(formencode.validators.OneOf(
597
["lecturer", "tutor", "student"]),
598
RoleEnrolmentValidator(),
599
formencode.validators.UnicodeString())
602
class EnrolmentEdit(BaseFormView):
603
"""A form to alter an enrolment's role."""
604
template = 'templates/enrolment-edit.html'
608
def populate_state(self, state):
609
state.offering = self.context.offering
611
def get_default_data(self, req):
612
return {'role': self.context.role}
616
return EnrolmentEditSchema()
618
def save_object(self, req, data):
619
self.context.role = data['role']
621
def get_return_url(self, obj):
622
return self.req.publisher.generate(
623
self.context.offering, EnrolmentsView)
625
def populate(self, req, ctx):
626
super(EnrolmentEdit, self).populate(req, ctx)
627
ctx['offering_perms'] = self.context.offering.get_permissions(
628
req.user, req.config)
631
class EnrolmentDelete(XHTMLView):
632
"""A form to alter an enrolment's role."""
633
template = 'templates/enrolment-delete.html'
637
def populate(self, req, ctx):
638
# If POSTing, delete delete delete.
639
if req.method == 'POST':
640
self.context.delete()
642
req.throw_redirect(req.publisher.generate(
643
self.context.offering, EnrolmentsView))
645
ctx['enrolment'] = self.context
361
648
class OfferingProjectsView(XHTMLView):
362
649
"""View the projects for an offering."""
363
650
template = 'templates/offering_projects.html'
364
651
permission = 'edit'
653
breadcrumb_text = 'Projects'
367
655
def populate(self, req, ctx):
368
656
self.plugin_styles[Plugin] = ["project.css"]
440
728
ctx['user'] = req.user
442
730
class Plugin(ViewPlugin, MediaPlugin):
443
forward_routes = (root_to_subject, subject_to_offering,
444
offering_to_project, offering_to_projectset)
445
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
731
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
732
offering_to_project, offering_to_projectset,
733
offering_to_enrolment)
735
subject_url, semester_url, offering_url, projectset_url, project_url,
447
738
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
739
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
448
740
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
741
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
742
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
449
743
(Subject, '+edit', SubjectEdit),
744
(Semester, '+edit', SemesterEdit),
450
745
(Offering, '+index', OfferingView),
451
746
(Offering, '+edit', OfferingEdit),
747
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
452
748
(Offering, ('+enrolments', '+index'), EnrolmentsView),
453
749
(Offering, ('+enrolments', '+new'), EnrolView),
750
(Enrolment, '+edit', EnrolmentEdit),
751
(Enrolment, '+delete', EnrolmentDelete),
454
752
(Offering, ('+projects', '+index'), OfferingProjectsView),
455
753
(Project, '+index', ProjectView),