49
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
50
47
from ivle.webapp.admin.offeringservice import OfferingRESTView
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
48
from ivle.webapp.admin.publishing import (root_to_subject,
52
49
subject_to_offering, offering_to_projectset, offering_to_project,
53
offering_to_enrolment, subject_url, semester_url, offering_url,
54
projectset_url, project_url, enrolment_url)
50
subject_url, offering_url, projectset_url, project_url)
55
51
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
56
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
58
from ivle.webapp.core import Plugin as CorePlugin
52
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
59
53
from ivle.webapp.groups import GroupsView
60
from ivle.webapp.media import media_url
61
54
from ivle.webapp.tutorial import Plugin as TutorialPlugin
63
56
class SubjectsView(XHTMLView):
64
57
'''The view of the list of subjects.'''
65
58
template = 'templates/subjects.html'
67
breadcrumb_text = "Subjects"
69
61
def authorize(self, req):
70
62
return req.user is not None
72
64
def populate(self, req, ctx):
74
65
ctx['user'] = req.user
75
66
ctx['semesters'] = []
77
67
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
78
68
Desc(Semester.semester)):
86
76
ctx['semesters'].append((semester, offerings))
89
class SubjectsManage(XHTMLView):
90
'''Subject management view.'''
91
template = 'templates/subjects-manage.html'
94
def authorize(self, req):
95
return req.user is not None and req.user.admin
97
def populate(self, req, ctx):
99
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
100
ctx['SubjectView'] = SubjectView
101
ctx['SubjectEdit'] = SubjectEdit
102
ctx['SemesterEdit'] = SemesterEdit
104
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
105
ctx['semesters'] = req.store.find(Semester).order_by(
106
Semester.year, Semester.semester)
109
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
110
"""A FormEncode validator that checks that a subject name is unused.
112
The subject referenced by state.existing_subject is permitted
113
to hold that name. If any other object holds it, the input is rejected.
115
def __init__(self, matching=None):
116
self.matching = matching
118
def _to_python(self, value, state):
119
if (state.store.find(
120
Subject, short_name=value).one() not in
121
(None, state.existing_subject)):
122
raise formencode.Invalid(
123
'Short name already taken', value, state)
127
class SubjectSchema(formencode.Schema):
128
short_name = formencode.All(
129
SubjectShortNameUniquenessValidator(),
130
URLNameValidator(not_empty=True))
131
name = formencode.validators.UnicodeString(not_empty=True)
132
code = formencode.validators.UnicodeString(not_empty=True)
135
class SubjectFormView(BaseFormView):
136
"""An abstract form to add or edit a subject."""
139
def authorize(self, req):
140
return req.user is not None and req.user.admin
142
def populate_state(self, state):
143
state.existing_subject = None
147
return SubjectSchema()
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 = URLNameValidator()
205
semester = URLNameValidator()
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']
268
class SubjectView(XHTMLView):
269
'''The view of the list of offerings in a given subject.'''
270
template = 'templates/subject.html'
273
def authorize(self, req):
274
return req.user is not None
276
def populate(self, req, ctx):
277
ctx['context'] = self.context
279
ctx['user'] = req.user
280
ctx['offerings'] = list(self.context.offerings)
281
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
282
ctx['SubjectEdit'] = SubjectEdit
283
ctx['SubjectOfferingNew'] = SubjectOfferingNew
79
def format_submission_principal(user, principal):
80
"""Render a list of users to fit in the offering project listing.
82
Given a user and a list of submitters, returns 'solo' if the
83
only submitter is the user, or a string of the form
84
'with A, B and C' if there are any other submitters.
86
If submitters is None, we assume that the list of members could
87
not be determined, so we just return 'group'.
95
display_names = sorted(
96
member.display_name for member in principal.members
97
if member is not user)
99
if len(display_names) == 0:
100
return 'solo (%s)' % principal.name
101
elif len(display_names) == 1:
102
return 'with %s (%s)' % (display_names[0], principal.name)
103
elif len(display_names) > 5:
104
return 'with %d others (%s)' % (len(display_names), principal.name)
106
return 'with %s and %s (%s)' % (', '.join(display_names[:-1]),
107
display_names[-1], principal.name)
286
110
class OfferingView(XHTMLView):
326
147
problems_done, problems_total))
329
class SubjectValidator(formencode.FancyValidator):
330
"""A FormEncode validator that turns a subject name into a subject.
332
The state must have a 'store' attribute, which is the Storm store
335
def _to_python(self, value, state):
336
subject = state.store.find(Subject, short_name=value).one()
340
raise formencode.Invalid('Subject does not exist', value, state)
343
class SemesterValidator(formencode.FancyValidator):
344
"""A FormEncode validator that turns a string into a semester.
346
The string should be of the form 'year/semester', eg. '2009/1'.
348
The state must have a 'store' attribute, which is the Storm store
351
def _to_python(self, value, state):
353
year, semester = value.split('/')
355
year = semester = None
357
semester = state.store.find(
358
Semester, year=year, semester=semester).one()
362
raise formencode.Invalid('Semester does not exist', value, state)
365
class OfferingUniquenessValidator(formencode.FancyValidator):
366
"""A FormEncode validator that checks that an offering is unique.
368
There cannot be more than one offering in the same year and semester.
370
The offering referenced by state.existing_offering is permitted to
371
hold that year and semester tuple. If any other object holds it, the
374
def _to_python(self, value, state):
375
if (state.store.find(
376
Offering, subject=value['subject'],
377
semester=value['semester']).one() not in
378
(None, state.existing_offering)):
379
raise formencode.Invalid(
380
'Offering already exists', value, state)
384
150
class OfferingSchema(formencode.Schema):
385
151
description = formencode.validators.UnicodeString(
386
152
if_missing=None, not_empty=False)
387
153
url = formencode.validators.URL(if_missing=None, not_empty=False)
388
show_worksheet_marks = formencode.validators.StringBoolean(
392
class OfferingAdminSchema(OfferingSchema):
393
subject = formencode.All(
394
SubjectValidator(), formencode.validators.UnicodeString())
395
semester = formencode.All(
396
SemesterValidator(), formencode.validators.UnicodeString())
397
chained_validators = [OfferingUniquenessValidator()]
400
class OfferingEdit(BaseFormView):
156
class OfferingEdit(XHTMLView):
401
157
"""A form to edit an offering's details."""
402
158
template = 'templates/offering-edit.html'
404
159
permission = 'edit'
408
if self.req.user.admin:
409
return OfferingAdminSchema()
161
def filter(self, stream, ctx):
162
return stream | HTMLFormFiller(data=ctx['data'])
164
def populate(self, req, ctx):
165
if req.method == 'POST':
166
data = dict(req.get_fieldstorage())
168
validator = OfferingSchema()
169
data = validator.to_python(data, state=req)
171
self.context.url = unicode(data['url']) if data['url'] else None
172
self.context.description = data['description']
174
req.throw_redirect(req.publisher.generate(self.context))
175
except formencode.Invalid, e:
176
errors = e.unpack_errors()
411
return OfferingSchema()
413
def populate(self, req, ctx):
414
super(OfferingEdit, self).populate(req, ctx)
415
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
416
ctx['semesters'] = req.store.find(Semester).order_by(
417
Semester.year, Semester.semester)
418
ctx['force_subject'] = None
420
def populate_state(self, state):
421
state.existing_offering = self.context
423
def get_default_data(self, req):
425
'subject': self.context.subject.short_name,
426
'semester': self.context.semester.year + '/' +
427
self.context.semester.semester,
428
'url': self.context.url,
429
'description': self.context.description,
430
'show_worksheet_marks': self.context.show_worksheet_marks,
179
'url': self.context.url,
180
'description': self.context.description,
433
def save_object(self, req, data):
435
self.context.subject = data['subject']
436
self.context.semester = data['semester']
437
self.context.description = data['description']
438
self.context.url = unicode(data['url']) if data['url'] else None
439
self.context.show_worksheet_marks = data['show_worksheet_marks']
443
class OfferingNew(BaseFormView):
444
"""A form to create an offering."""
445
template = 'templates/offering-new.html'
448
def authorize(self, req):
449
return req.user is not None and req.user.admin
453
return OfferingAdminSchema()
455
def populate(self, req, ctx):
456
super(OfferingNew, self).populate(req, ctx)
457
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
458
ctx['semesters'] = req.store.find(Semester).order_by(
459
Semester.year, Semester.semester)
460
ctx['force_subject'] = None
462
def populate_state(self, state):
463
state.existing_offering = None
465
def get_default_data(self, req):
468
def save_object(self, req, data):
469
new_offering = Offering()
470
new_offering.subject = data['subject']
471
new_offering.semester = data['semester']
472
new_offering.description = data['description']
473
new_offering.url = unicode(data['url']) if data['url'] else None
474
new_offering.show_worksheet_marks = data['show_worksheet_marks']
476
req.store.add(new_offering)
479
class SubjectOfferingNew(OfferingNew):
480
"""A form to create an offering for a given subject."""
481
# Identical to OfferingNew, except it forces the subject to be the subject
483
def populate(self, req, ctx):
484
super(SubjectOfferingNew, self).populate(req, ctx)
485
ctx['force_subject'] = self.context
487
class OfferingCloneWorksheetsSchema(formencode.Schema):
488
subject = formencode.All(
489
SubjectValidator(), formencode.validators.UnicodeString())
490
semester = formencode.All(
491
SemesterValidator(), formencode.validators.UnicodeString())
494
class OfferingCloneWorksheets(BaseFormView):
495
"""A form to clone worksheets from one offering to another."""
496
template = 'templates/offering-clone-worksheets.html'
499
def authorize(self, req):
500
return req.user is not None and req.user.admin
504
return OfferingCloneWorksheetsSchema()
506
def populate(self, req, ctx):
507
super(OfferingCloneWorksheets, self).populate(req, ctx)
508
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
509
ctx['semesters'] = req.store.find(Semester).order_by(
510
Semester.year, Semester.semester)
512
def get_default_data(self, req):
515
def save_object(self, req, data):
516
if self.context.worksheets.count() > 0:
518
"Cannot clone to target with existing worksheets.")
519
offering = req.store.find(
520
Offering, subject=data['subject'], semester=data['semester']).one()
522
raise BadRequest("No such offering.")
523
if offering.worksheets.count() == 0:
524
raise BadRequest("Source offering has no worksheets.")
526
self.context.clone_worksheets(offering)
184
ctx['data'] = data or {}
185
ctx['context'] = self.context
186
ctx['errors'] = errors
530
189
class UserValidator(formencode.FancyValidator):
619
267
ctx['data'] = data or {}
620
268
ctx['offering'] = self.context
621
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
269
ctx['roles_auth'] = self.context.get_permissions(req.user)
622
270
ctx['errors'] = errors
623
# If all of the fields validated, set the global form error.
624
if isinstance(errors, basestring):
625
ctx['error_value'] = errors
628
class EnrolmentEditSchema(formencode.Schema):
629
role = formencode.All(formencode.validators.OneOf(
630
["lecturer", "tutor", "student"]),
631
RoleEnrolmentValidator(),
632
formencode.validators.UnicodeString())
635
class EnrolmentEdit(BaseFormView):
636
"""A form to alter an enrolment's role."""
637
template = 'templates/enrolment-edit.html'
641
def populate_state(self, state):
642
state.offering = self.context.offering
644
def get_default_data(self, req):
645
return {'role': self.context.role}
649
return EnrolmentEditSchema()
651
def save_object(self, req, data):
652
self.context.role = data['role']
654
def get_return_url(self, obj):
655
return self.req.publisher.generate(
656
self.context.offering, EnrolmentsView)
658
def populate(self, req, ctx):
659
super(EnrolmentEdit, self).populate(req, ctx)
660
ctx['offering_perms'] = self.context.offering.get_permissions(
661
req.user, req.config)
664
class EnrolmentDelete(XHTMLView):
665
"""A form to alter an enrolment's role."""
666
template = 'templates/enrolment-delete.html'
670
def populate(self, req, ctx):
671
# If POSTing, delete delete delete.
672
if req.method == 'POST':
673
self.context.delete()
675
req.throw_redirect(req.publisher.generate(
676
self.context.offering, EnrolmentsView))
678
ctx['enrolment'] = self.context
681
272
class OfferingProjectsView(XHTMLView):
682
273
"""View the projects for an offering."""
683
274
template = 'templates/offering_projects.html'
684
275
permission = 'edit'
686
breadcrumb_text = 'Projects'
688
278
def populate(self, req, ctx):
689
279
self.plugin_styles[Plugin] = ["project.css"]
761
351
ctx['user'] = req.user
763
353
class Plugin(ViewPlugin, MediaPlugin):
764
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
765
offering_to_project, offering_to_projectset,
766
offering_to_enrolment)
768
subject_url, semester_url, offering_url, projectset_url, project_url,
354
forward_routes = (root_to_subject, subject_to_offering,
355
offering_to_project, offering_to_projectset)
356
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
771
358
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
772
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
773
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
774
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
775
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
776
(Subject, '+index', SubjectView),
777
(Subject, '+edit', SubjectEdit),
778
(Subject, '+new-offering', SubjectOfferingNew),
779
(Semester, '+edit', SemesterEdit),
780
359
(Offering, '+index', OfferingView),
781
360
(Offering, '+edit', OfferingEdit),
782
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
783
361
(Offering, ('+enrolments', '+index'), EnrolmentsView),
784
362
(Offering, ('+enrolments', '+new'), EnrolView),
785
(Enrolment, '+edit', EnrolmentEdit),
786
(Enrolment, '+delete', EnrolmentDelete),
787
363
(Offering, ('+projects', '+index'), OfferingProjectsView),
788
364
(Project, '+index', ProjectView),