23
23
# A sample / testing application for IVLE.
29
from storm.locals import Desc
31
from storm.locals import Desc, Store
30
33
from genshi.filters import HTMLFormFiller
34
from genshi.template import Context, TemplateLoader
36
import formencode.validators
38
from ivle.webapp.base.forms import BaseFormView
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
33
40
from ivle.webapp.base.xhtml import XHTMLView
34
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
35
from ivle.webapp.errors import NotFound
36
from ivle.database import Subject, Semester, Offering, Enrolment, User
41
from ivle.webapp.errors import BadRequest
42
from ivle.webapp import ApplicationRoot
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
45
ProjectSet, Project, ProjectSubmission
37
46
from ivle import util
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
52
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)
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
56
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
57
from ivle.webapp.core import Plugin as CorePlugin
58
from ivle.webapp.groups import GroupsView
59
from ivle.webapp.media import media_url
60
from ivle.webapp.tutorial import Plugin as TutorialPlugin
40
62
class SubjectsView(XHTMLView):
41
63
'''The view of the list of subjects.'''
42
template = 'subjects.html'
64
template = 'templates/subjects.html'
45
67
def authorize(self, req):
46
68
return req.user is not None
48
70
def populate(self, req, ctx):
49
72
ctx['user'] = req.user
50
73
ctx['semesters'] = []
51
75
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
52
76
Desc(Semester.semester)):
53
enrolments = semester.enrolments.find(user=req.user)
54
if enrolments.count():
55
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
287
ctx['EnrolmentsView'] = EnrolmentsView
289
# As we go, calculate the total score for this subject
290
# (Assessable worksheets only, mandatory problems only)
292
ctx['worksheets'], problems_total, problems_done = (
293
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
294
req.store, req.user, self.context))
296
ctx['exercises_total'] = problems_total
297
ctx['exercises_done'] = problems_done
298
if problems_total > 0:
299
if problems_done >= problems_total:
300
ctx['worksheets_complete_class'] = "complete"
301
elif problems_done > 0:
302
ctx['worksheets_complete_class'] = "semicomplete"
304
ctx['worksheets_complete_class'] = "incomplete"
305
# Calculate the final percentage and mark for the subject
306
(ctx['exercises_pct'], ctx['worksheet_mark'],
307
ctx['worksheet_max_mark']) = (
308
ivle.worksheet.utils.calculate_mark(
309
problems_done, problems_total))
312
class SubjectValidator(formencode.FancyValidator):
313
"""A FormEncode validator that turns a subject name into a subject.
315
The state must have a 'store' attribute, which is the Storm store
318
def _to_python(self, value, state):
319
subject = state.store.find(Subject, short_name=value).one()
323
raise formencode.Invalid('Subject does not exist', value, state)
326
class SemesterValidator(formencode.FancyValidator):
327
"""A FormEncode validator that turns a string into a semester.
329
The string should be of the form 'year/semester', eg. '2009/1'.
331
The state must have a 'store' attribute, which is the Storm store
334
def _to_python(self, value, state):
336
year, semester = value.split('/')
338
year = semester = None
340
semester = state.store.find(
341
Semester, year=year, semester=semester).one()
345
raise formencode.Invalid('Semester does not exist', value, state)
348
class OfferingUniquenessValidator(formencode.FancyValidator):
349
"""A FormEncode validator that checks that an offering is unique.
351
There cannot be more than one offering in the same year and semester.
353
The offering referenced by state.existing_offering is permitted to
354
hold that year and semester tuple. If any other object holds it, the
357
def _to_python(self, value, state):
358
if (state.store.find(
359
Offering, subject=value['subject'],
360
semester=value['semester']).one() not in
361
(None, state.existing_offering)):
362
raise formencode.Invalid(
363
'Offering already exists', value, state)
367
class OfferingSchema(formencode.Schema):
368
description = formencode.validators.UnicodeString(
369
if_missing=None, not_empty=False)
370
url = formencode.validators.URL(if_missing=None, not_empty=False)
373
class OfferingAdminSchema(OfferingSchema):
374
subject = formencode.All(
375
SubjectValidator(), formencode.validators.UnicodeString())
376
semester = formencode.All(
377
SemesterValidator(), formencode.validators.UnicodeString())
378
chained_validators = [OfferingUniquenessValidator()]
381
class OfferingEdit(BaseFormView):
382
"""A form to edit an offering's details."""
383
template = 'templates/offering-edit.html'
389
if self.req.user.admin:
390
return OfferingAdminSchema()
392
return OfferingSchema()
394
def populate(self, req, ctx):
395
super(OfferingEdit, self).populate(req, ctx)
396
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
397
ctx['semesters'] = req.store.find(Semester).order_by(
398
Semester.year, Semester.semester)
400
def populate_state(self, state):
401
state.existing_offering = self.context
403
def get_default_data(self, req):
405
'subject': self.context.subject.short_name,
406
'semester': self.context.semester.year + '/' +
407
self.context.semester.semester,
408
'url': self.context.url,
409
'description': self.context.description,
412
def save_object(self, req, data):
414
self.context.subject = data['subject']
415
self.context.semester = data['semester']
416
self.context.description = data['description']
417
self.context.url = unicode(data['url']) if data['url'] else None
421
class OfferingNew(BaseFormView):
422
"""A form to create an offering."""
423
template = 'templates/offering-new.html'
426
def authorize(self, req):
427
return req.user is not None and req.user.admin
431
return OfferingAdminSchema()
433
def populate(self, req, ctx):
434
super(OfferingNew, self).populate(req, ctx)
435
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
436
ctx['semesters'] = req.store.find(Semester).order_by(
437
Semester.year, Semester.semester)
439
def populate_state(self, state):
440
state.existing_offering = None
442
def get_default_data(self, req):
445
def save_object(self, req, data):
446
new_offering = Offering()
447
new_offering.subject = data['subject']
448
new_offering.semester = data['semester']
449
new_offering.description = data['description']
450
new_offering.url = unicode(data['url']) if data['url'] else None
452
req.store.add(new_offering)
456
class OfferingCloneWorksheetsSchema(formencode.Schema):
457
subject = formencode.All(
458
SubjectValidator(), formencode.validators.UnicodeString())
459
semester = formencode.All(
460
SemesterValidator(), formencode.validators.UnicodeString())
463
class OfferingCloneWorksheets(BaseFormView):
464
"""A form to clone worksheets from one offering to another."""
465
template = 'templates/offering-clone-worksheets.html'
468
def authorize(self, req):
469
return req.user is not None and req.user.admin
473
return OfferingCloneWorksheetsSchema()
475
def populate(self, req, ctx):
476
super(OfferingCloneWorksheets, self).populate(req, ctx)
477
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
478
ctx['semesters'] = req.store.find(Semester).order_by(
479
Semester.year, Semester.semester)
481
def get_default_data(self, req):
484
def save_object(self, req, data):
485
if self.context.worksheets.count() > 0:
487
"Cannot clone to target with existing worksheets.")
488
offering = req.store.find(
489
Offering, subject=data['subject'], semester=data['semester']).one()
491
raise BadRequest("No such offering.")
492
if offering.worksheets.count() == 0:
493
raise BadRequest("Source offering has no worksheets.")
495
self.context.clone_worksheets(offering)
58
499
class UserValidator(formencode.FancyValidator):
123
585
ctx['data'] = data or {}
124
586
ctx['offering'] = self.context
587
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
125
588
ctx['errors'] = errors
591
class EnrolmentEditSchema(formencode.Schema):
592
role = formencode.All(formencode.validators.OneOf(
593
["lecturer", "tutor", "student"]),
594
RoleEnrolmentValidator(),
595
formencode.validators.UnicodeString())
598
class EnrolmentEdit(BaseFormView):
599
"""A form to alter an enrolment's role."""
600
template = 'templates/enrolment-edit.html'
604
def populate_state(self, state):
605
state.offering = self.context.offering
607
def get_default_data(self, req):
608
return {'role': self.context.role}
612
return EnrolmentEditSchema()
614
def save_object(self, req, data):
615
self.context.role = data['role']
617
def get_return_url(self, obj):
618
return self.req.publisher.generate(
619
self.context.offering, EnrolmentsView)
621
def populate(self, req, ctx):
622
super(EnrolmentEdit, self).populate(req, ctx)
623
ctx['offering_perms'] = self.context.offering.get_permissions(
624
req.user, req.config)
627
class EnrolmentDelete(XHTMLView):
628
"""A form to alter an enrolment's role."""
629
template = 'templates/enrolment-delete.html'
633
def populate(self, req, ctx):
634
# If POSTing, delete delete delete.
635
if req.method == 'POST':
636
self.context.delete()
638
req.throw_redirect(req.publisher.generate(
639
self.context.offering, EnrolmentsView))
641
ctx['enrolment'] = self.context
644
class OfferingProjectsView(XHTMLView):
645
"""View the projects for an offering."""
646
template = 'templates/offering_projects.html'
650
def populate(self, req, ctx):
651
self.plugin_styles[Plugin] = ["project.css"]
652
self.plugin_scripts[Plugin] = ["project.js"]
654
ctx['offering'] = self.context
655
ctx['projectsets'] = []
656
ctx['OfferingRESTView'] = OfferingRESTView
658
#Open the projectset Fragment, and render it for inclusion
659
#into the ProjectSets page
660
#XXX: This could be a lot cleaner
661
loader = genshi.template.TemplateLoader(".", auto_reload=True)
663
set_fragment = os.path.join(os.path.dirname(__file__),
664
"templates/projectset_fragment.html")
665
project_fragment = os.path.join(os.path.dirname(__file__),
666
"templates/project_fragment.html")
668
for projectset in self.context.project_sets:
669
settmpl = loader.load(set_fragment)
672
setCtx['projectset'] = projectset
673
setCtx['projects'] = []
674
setCtx['GroupsView'] = GroupsView
675
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
677
for project in projectset.projects:
678
projecttmpl = loader.load(project_fragment)
679
projectCtx = Context()
680
projectCtx['req'] = req
681
projectCtx['project'] = project
683
setCtx['projects'].append(
684
projecttmpl.generate(projectCtx))
686
ctx['projectsets'].append(settmpl.generate(setCtx))
689
class ProjectView(XHTMLView):
690
"""View the submissions for a ProjectSet"""
691
template = "templates/project.html"
692
permission = "view_project_submissions"
695
def build_subversion_url(self, svnroot, submission):
696
princ = submission.assessed.principal
698
if isinstance(princ, User):
699
path = 'users/%s' % princ.login
701
path = 'groups/%s_%s_%s_%s' % (
702
princ.project_set.offering.subject.short_name,
703
princ.project_set.offering.semester.year,
704
princ.project_set.offering.semester.semester,
707
return urlparse.urljoin(
709
os.path.join(path, submission.path[1:] if
710
submission.path.startswith(os.sep) else
713
def populate(self, req, ctx):
714
self.plugin_styles[Plugin] = ["project.css"]
717
ctx['GroupsView'] = GroupsView
718
ctx['EnrolView'] = EnrolView
719
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
720
ctx['build_subversion_url'] = self.build_subversion_url
721
ctx['svn_addr'] = req.config['urls']['svn_addr']
722
ctx['project'] = self.context
723
ctx['user'] = req.user
128
725
class Plugin(ViewPlugin, MediaPlugin):
130
('subjects/', SubjectsView),
131
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
726
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
727
offering_to_project, offering_to_projectset,
728
offering_to_enrolment)
730
subject_url, semester_url, offering_url, projectset_url, project_url,
733
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
734
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
735
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
736
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
737
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
738
(Subject, '+edit', SubjectEdit),
739
(Semester, '+edit', SemesterEdit),
740
(Offering, '+index', OfferingView),
741
(Offering, '+edit', OfferingEdit),
742
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
743
(Offering, ('+enrolments', '+index'), EnrolmentsView),
744
(Offering, ('+enrolments', '+new'), EnrolView),
745
(Enrolment, '+edit', EnrolmentEdit),
746
(Enrolment, '+delete', EnrolmentDelete),
747
(Offering, ('+projects', '+index'), OfferingProjectsView),
748
(Project, '+index', ProjectView),
750
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
751
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
754
breadcrumbs = {Subject: SubjectBreadcrumb,
755
Offering: OfferingBreadcrumb,
756
User: UserBreadcrumb,
757
Project: ProjectBreadcrumb,
135
761
('subjects', 'Subjects',