23
23
# A sample / testing application for IVLE.
31
from storm.locals import Desc, Store
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
40
from ivle.webapp.base.xhtml import XHTMLView
41
from ivle.webapp import ApplicationRoot
43
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
44
ProjectSet, Project, ProjectSubmission
30
45
from ivle import util
33
import genshi.template
36
"""Handler for the Subjects application. Links to subject home pages."""
38
req.styles = ["media/subjects/subjects.css"]
39
ctx = genshi.template.Context()
41
# This is represented as a directory. Redirect and add a slash if it is
43
if req.uri[-1] != '/':
44
req.throw_redirect(req.uri + '/')
45
ctx['whichpage'] = "toplevel"
46
handle_toplevel_menu(req, ctx)
48
ctx['whichpage'] = "subject"
49
handle_subject_page(req, req.path, ctx)
51
loader = genshi.template.TemplateLoader(".", auto_reload=True)
52
tmpl = loader.load(util.make_local_path("apps/subjects/template.html"))
53
req.write(tmpl.generate(ctx).render('html')) #'xhtml', doctype='xhtml'))
55
def handle_toplevel_menu(req, ctx):
57
enrolled_subjects = req.user.subjects
58
unenrolled_subjects = [subject for subject in
59
req.store.find(ivle.database.Subject)
60
if subject not in enrolled_subjects]
62
ctx['enrolled_subjects'] = []
63
ctx['other_subjects'] = []
65
req.content_type = "text/html"
66
req.write_html_head_foot = True
68
for subject in enrolled_subjects:
70
new_subj['name'] = subject.name
71
new_subj['url'] = subject.url
72
ctx['enrolled_subjects'].append(new_subj)
74
if len(unenrolled_subjects) > 0:
75
for subject in unenrolled_subjects:
77
new_subj['name'] = subject.name
78
new_subj['url'] = subject.url
79
ctx['other_subjects'].append(new_subj)
82
def handle_subject_page(req, path, ctx):
83
req.content_type = "text/html"
84
req.write_html_head_foot = True # Have dispatch print head and foot
86
# Just make the iframe pointing to media/subjects
87
ctx['serve_loc'] = urllib.quote(util.make_path(os.path.join('media', 'subjects', path)))
48
from ivle.webapp.admin.projectservice import ProjectSetRESTView
49
from ivle.webapp.admin.offeringservice import OfferingRESTView
50
from ivle.webapp.admin.publishing import (root_to_subject,
51
subject_to_offering, offering_to_projectset, offering_to_project,
52
subject_url, offering_url, projectset_url, project_url)
53
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
54
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
55
from ivle.webapp.core import Plugin as CorePlugin
56
from ivle.webapp.groups import GroupsView
57
from ivle.webapp.media import media_url
58
from ivle.webapp.tutorial import Plugin as TutorialPlugin
60
class SubjectsView(XHTMLView):
61
'''The view of the list of subjects.'''
62
template = 'templates/subjects.html'
65
def authorize(self, req):
66
return req.user is not None
68
def populate(self, req, ctx):
70
ctx['user'] = req.user
72
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
73
ctx['SubjectEdit'] = SubjectEdit
75
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
76
Desc(Semester.semester)):
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(XHTMLView):
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 filter(self, stream, ctx):
125
return stream | HTMLFormFiller(data=ctx['data'])
127
def populate_state(self, state):
128
state.existing_subject = None
130
def populate(self, req, ctx):
131
if req.method == 'POST':
132
data = dict(req.get_fieldstorage())
134
validator = SubjectSchema()
135
self.populate_state(req)
136
data = validator.to_python(data, state=req)
138
subject = self.update_subject_object(req, data)
141
req.throw_redirect(req.publisher.generate(subject))
142
except formencode.Invalid, e:
143
errors = e.unpack_errors()
145
data = self.get_default_data(req)
151
ctx['context'] = self.context
152
ctx['data'] = data or {}
153
ctx['errors'] = errors
156
class SubjectNew(SubjectFormView):
157
"""A form to create a subject."""
158
template = 'templates/subject-new.html'
160
def populate_state(self, state):
161
state.existing_subject = self.context
163
def get_default_data(self, req):
166
def update_subject_object(self, req, data):
167
new_subject = Subject()
168
new_subject.short_name = data['short_name']
169
new_subject.name = data['name']
170
new_subject.code = data['code']
172
req.store.add(new_subject)
176
class SubjectEdit(SubjectFormView):
177
"""A form to edit a subject."""
178
template = 'templates/subject-edit.html'
180
def populate_state(self, state):
181
state.existing_subject = self.context
183
def get_default_data(self, req):
185
'short_name': self.context.short_name,
186
'name': self.context.name,
187
'code': self.context.code,
190
def update_subject_object(self, req, data):
191
self.context.short_name = data['short_name']
192
self.context.name = data['name']
193
self.context.code = data['code']
198
class SemesterUniquenessValidator(formencode.FancyValidator):
199
"""A FormEncode validator that checks that a semester is unique.
201
There cannot be more than one semester for the same year and semester.
203
def _to_python(self, value, state):
204
if (state.store.find(
205
Semester, year=value['year'], semester=value['semester']
207
raise formencode.Invalid(
208
'Semester already exists', value, state)
212
class SemesterSchema(formencode.Schema):
213
year = formencode.validators.UnicodeString()
214
semester = formencode.validators.UnicodeString()
215
chained_validators = [SemesterUniquenessValidator()]
218
class SemesterNew(BaseFormView):
219
"""A form to create a semester."""
220
template = 'templates/semester-new.html'
223
def authorize(self, req):
224
return req.user is not None and req.user.admin
228
return SemesterSchema()
230
def get_default_data(self, req):
233
def save_object(self, req, data):
234
new_semester = Semester()
235
new_semester.year = data['year']
236
new_semester.semester = data['semester']
238
req.store.add(new_semester)
241
def get_return_url(self, obj):
245
class OfferingView(XHTMLView):
246
"""The home page of an offering."""
247
template = 'templates/offering.html'
251
def populate(self, req, ctx):
252
# Need the worksheet result styles.
253
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
254
ctx['context'] = self.context
256
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
257
ctx['format_submission_principal'] = util.format_submission_principal
258
ctx['format_datetime'] = ivle.date.make_date_nice
259
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
260
ctx['OfferingEdit'] = OfferingEdit
262
# As we go, calculate the total score for this subject
263
# (Assessable worksheets only, mandatory problems only)
265
ctx['worksheets'], problems_total, problems_done = (
266
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
267
req.store, req.user, self.context))
269
ctx['exercises_total'] = problems_total
270
ctx['exercises_done'] = problems_done
271
if problems_total > 0:
272
if problems_done >= problems_total:
273
ctx['worksheets_complete_class'] = "complete"
274
elif problems_done > 0:
275
ctx['worksheets_complete_class'] = "semicomplete"
277
ctx['worksheets_complete_class'] = "incomplete"
278
# Calculate the final percentage and mark for the subject
279
(ctx['exercises_pct'], ctx['worksheet_mark'],
280
ctx['worksheet_max_mark']) = (
281
ivle.worksheet.utils.calculate_mark(
282
problems_done, problems_total))
285
class SubjectValidator(formencode.FancyValidator):
286
"""A FormEncode validator that turns a subject name into a subject.
288
The state must have a 'store' attribute, which is the Storm store
291
def _to_python(self, value, state):
292
subject = state.store.find(Subject, short_name=value).one()
296
raise formencode.Invalid('Subject does not exist', value, state)
299
class SemesterValidator(formencode.FancyValidator):
300
"""A FormEncode validator that turns a string into a semester.
302
The string should be of the form 'year/semester', eg. '2009/1'.
304
The state must have a 'store' attribute, which is the Storm store
307
def _to_python(self, value, state):
309
year, semester = value.split('/')
311
year = semester = None
313
semester = state.store.find(
314
Semester, year=year, semester=semester).one()
318
raise formencode.Invalid('Semester does not exist', value, state)
321
class OfferingUniquenessValidator(formencode.FancyValidator):
322
"""A FormEncode validator that checks that an offering is unique.
324
There cannot be more than one offering in the same year and semester.
326
The offering referenced by state.existing_offering is permitted to
327
hold that year and semester tuple. If any other object holds it, the
330
def _to_python(self, value, state):
331
if (state.store.find(
332
Offering, subject=value['subject'],
333
semester=value['semester']).one() not in
334
(None, state.existing_offering)):
335
raise formencode.Invalid(
336
'Offering already exists', value, state)
340
class OfferingSchema(formencode.Schema):
341
description = formencode.validators.UnicodeString(
342
if_missing=None, not_empty=False)
343
url = formencode.validators.URL(if_missing=None, not_empty=False)
346
class OfferingAdminSchema(OfferingSchema):
347
subject = formencode.All(
348
SubjectValidator(), formencode.validators.UnicodeString())
349
semester = formencode.All(
350
SemesterValidator(), formencode.validators.UnicodeString())
351
chained_validators = [OfferingUniquenessValidator()]
354
class OfferingEdit(BaseFormView):
355
"""A form to edit an offering's details."""
356
template = 'templates/offering-edit.html'
362
if self.req.user.admin:
363
return OfferingAdminSchema()
365
return OfferingSchema()
367
def populate(self, req, ctx):
368
super(OfferingEdit, self).populate(req, ctx)
369
ctx['subjects'] = req.store.find(Subject)
370
ctx['semesters'] = req.store.find(Semester)
372
def populate_state(self, state):
373
state.existing_offering = self.context
375
def get_default_data(self, req):
377
'subject': self.context.subject.short_name,
378
'semester': self.context.semester.year + '/' +
379
self.context.semester.semester,
380
'url': self.context.url,
381
'description': self.context.description,
384
def save_object(self, req, data):
386
self.context.subject = data['subject']
387
self.context.semester = data['semester']
388
self.context.description = data['description']
389
self.context.url = unicode(data['url']) if data['url'] else None
393
class OfferingNew(BaseFormView):
394
"""A form to create an offering."""
395
template = 'templates/offering-new.html'
398
def authorize(self, req):
399
return req.user is not None and req.user.admin
403
return OfferingAdminSchema()
405
def populate(self, req, ctx):
406
super(OfferingNew, self).populate(req, ctx)
407
ctx['subjects'] = req.store.find(Subject)
408
ctx['semesters'] = req.store.find(Semester)
410
def populate_state(self, state):
411
state.existing_offering = None
413
def get_default_data(self, req):
416
def save_object(self, req, data):
417
new_offering = Offering()
418
new_offering.subject = data['subject']
419
new_offering.semester = data['semester']
420
new_offering.description = data['description']
421
new_offering.url = unicode(data['url']) if data['url'] else None
423
req.store.add(new_offering)
427
class UserValidator(formencode.FancyValidator):
428
"""A FormEncode validator that turns a username into a user.
430
The state must have a 'store' attribute, which is the Storm store
432
def _to_python(self, value, state):
433
user = User.get_by_login(state.store, value)
437
raise formencode.Invalid('User does not exist', value, state)
440
class NoEnrolmentValidator(formencode.FancyValidator):
441
"""A FormEncode validator that ensures absence of an enrolment.
443
The state must have an 'offering' attribute.
445
def _to_python(self, value, state):
446
if state.offering.get_enrolment(value):
447
raise formencode.Invalid('User already enrolled', value, state)
451
class RoleEnrolmentValidator(formencode.FancyValidator):
452
"""A FormEncode validator that checks permission to enrol users with a
455
The state must have an 'offering' attribute.
457
def _to_python(self, value, state):
458
if (("enrol_" + value) not in
459
state.offering.get_permissions(state.user, state.config)):
460
raise formencode.Invalid('Not allowed to assign users that role',
465
class EnrolSchema(formencode.Schema):
466
user = formencode.All(NoEnrolmentValidator(), UserValidator())
467
role = formencode.All(formencode.validators.OneOf(
468
["lecturer", "tutor", "student"]),
469
RoleEnrolmentValidator(),
470
formencode.validators.UnicodeString())
473
class EnrolmentsView(XHTMLView):
474
"""A page which displays all users enrolled in an offering."""
475
template = 'templates/enrolments.html'
479
def populate(self, req, ctx):
480
ctx['offering'] = self.context
482
class EnrolView(XHTMLView):
483
"""A form to enrol a user in an offering."""
484
template = 'templates/enrol.html'
488
def filter(self, stream, ctx):
489
return stream | HTMLFormFiller(data=ctx['data'])
491
def populate(self, req, ctx):
492
if req.method == 'POST':
493
data = dict(req.get_fieldstorage())
495
validator = EnrolSchema()
496
req.offering = self.context # XXX: Getting into state.
497
data = validator.to_python(data, state=req)
498
self.context.enrol(data['user'], data['role'])
500
req.throw_redirect(req.uri)
501
except formencode.Invalid, e:
502
errors = e.unpack_errors()
507
ctx['data'] = data or {}
508
ctx['offering'] = self.context
509
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
510
ctx['errors'] = errors
512
class OfferingProjectsView(XHTMLView):
513
"""View the projects for an offering."""
514
template = 'templates/offering_projects.html'
518
def populate(self, req, ctx):
519
self.plugin_styles[Plugin] = ["project.css"]
520
self.plugin_scripts[Plugin] = ["project.js"]
522
ctx['offering'] = self.context
523
ctx['projectsets'] = []
524
ctx['OfferingRESTView'] = OfferingRESTView
526
#Open the projectset Fragment, and render it for inclusion
527
#into the ProjectSets page
528
#XXX: This could be a lot cleaner
529
loader = genshi.template.TemplateLoader(".", auto_reload=True)
531
set_fragment = os.path.join(os.path.dirname(__file__),
532
"templates/projectset_fragment.html")
533
project_fragment = os.path.join(os.path.dirname(__file__),
534
"templates/project_fragment.html")
536
for projectset in self.context.project_sets:
537
settmpl = loader.load(set_fragment)
540
setCtx['projectset'] = projectset
541
setCtx['projects'] = []
542
setCtx['GroupsView'] = GroupsView
543
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
545
for project in projectset.projects:
546
projecttmpl = loader.load(project_fragment)
547
projectCtx = Context()
548
projectCtx['req'] = req
549
projectCtx['project'] = project
551
setCtx['projects'].append(
552
projecttmpl.generate(projectCtx))
554
ctx['projectsets'].append(settmpl.generate(setCtx))
557
class ProjectView(XHTMLView):
558
"""View the submissions for a ProjectSet"""
559
template = "templates/project.html"
563
def build_subversion_url(self, svnroot, submission):
564
princ = submission.assessed.principal
566
if isinstance(princ, User):
567
path = 'users/%s' % princ.login
569
path = 'groups/%s_%s_%s_%s' % (
570
princ.project_set.offering.subject.short_name,
571
princ.project_set.offering.semester.year,
572
princ.project_set.offering.semester.semester,
575
return urlparse.urljoin(
577
os.path.join(path, submission.path[1:] if
578
submission.path.startswith(os.sep) else
581
def populate(self, req, ctx):
582
self.plugin_styles[Plugin] = ["project.css"]
585
ctx['GroupsView'] = GroupsView
586
ctx['EnrolView'] = EnrolView
587
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
588
ctx['build_subversion_url'] = self.build_subversion_url
589
ctx['svn_addr'] = req.config['urls']['svn_addr']
590
ctx['project'] = self.context
591
ctx['user'] = req.user
593
class Plugin(ViewPlugin, MediaPlugin):
594
forward_routes = (root_to_subject, subject_to_offering,
595
offering_to_project, offering_to_projectset)
596
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
598
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
599
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
600
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
601
(ApplicationRoot, ('subjects', '+new-semester'), SemesterNew),
602
(Subject, '+edit', SubjectEdit),
603
(Offering, '+index', OfferingView),
604
(Offering, '+edit', OfferingEdit),
605
(Offering, ('+enrolments', '+index'), EnrolmentsView),
606
(Offering, ('+enrolments', '+new'), EnrolView),
607
(Offering, ('+projects', '+index'), OfferingProjectsView),
608
(Project, '+index', ProjectView),
610
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
611
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
614
breadcrumbs = {Subject: SubjectBreadcrumb,
615
Offering: OfferingBreadcrumb,
616
User: UserBreadcrumb,
617
Project: ProjectBreadcrumb,
621
('subjects', 'Subjects',
622
'View subject content and complete worksheets',
623
'subjects.png', 'subjects', 5)
626
media = 'subject-media'