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
33
37
from ivle.webapp.base.xhtml import XHTMLView
34
38
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
39
from ivle.webapp import ApplicationRoot
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
ProjectSet, Project, ProjectSubmission
37
43
from ivle import util
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
47
from ivle.webapp.admin.offeringservice import OfferingRESTView
48
from ivle.webapp.admin.publishing import (root_to_subject,
49
subject_to_offering, offering_to_projectset, offering_to_project,
50
subject_url, offering_url, projectset_url, project_url)
51
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
52
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
53
from ivle.webapp.groups import GroupsView
54
from ivle.webapp.tutorial import Plugin as TutorialPlugin
40
56
class SubjectsView(XHTMLView):
41
57
'''The view of the list of subjects.'''
42
template = 'subjects.html'
58
template = 'templates/subjects.html'
45
61
def authorize(self, req):
46
62
return req.user is not None
48
64
def populate(self, req, ctx):
49
66
ctx['user'] = req.user
50
67
ctx['semesters'] = []
51
68
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
52
69
Desc(Semester.semester)):
53
enrolments = semester.enrolments.find(user=req.user)
54
if enrolments.count():
55
ctx['semesters'].append((semester, enrolments))
71
# For admins, show all subjects in the system
72
offerings = list(semester.offerings.find())
74
offerings = [enrolment.offering for enrolment in
75
semester.enrolments.find(user=req.user)]
77
ctx['semesters'].append((semester, offerings))
79
class OfferingView(XHTMLView):
80
"""The home page of an offering."""
81
template = 'templates/offering.html'
85
def populate(self, req, ctx):
86
# Need the worksheet result styles.
87
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
88
ctx['context'] = self.context
90
ctx['permissions'] = self.context.get_permissions(req.user)
91
ctx['format_submission_principal'] = util.format_submission_principal
92
ctx['format_datetime'] = ivle.date.make_date_nice
93
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
94
ctx['OfferingEdit'] = OfferingEdit
96
# As we go, calculate the total score for this subject
97
# (Assessable worksheets only, mandatory problems only)
99
ctx['worksheets'], problems_total, problems_done = (
100
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
101
req.store, req.user, self.context))
103
ctx['exercises_total'] = problems_total
104
ctx['exercises_done'] = problems_done
105
if problems_total > 0:
106
if problems_done >= problems_total:
107
ctx['worksheets_complete_class'] = "complete"
108
elif problems_done > 0:
109
ctx['worksheets_complete_class'] = "semicomplete"
111
ctx['worksheets_complete_class'] = "incomplete"
112
# Calculate the final percentage and mark for the subject
113
(ctx['exercises_pct'], ctx['worksheet_mark'],
114
ctx['worksheet_max_mark']) = (
115
ivle.worksheet.utils.calculate_mark(
116
problems_done, problems_total))
119
class OfferingSchema(formencode.Schema):
120
description = formencode.validators.UnicodeString(
121
if_missing=None, not_empty=False)
122
url = formencode.validators.URL(if_missing=None, not_empty=False)
125
class OfferingEdit(XHTMLView):
126
"""A form to edit an offering's details."""
127
template = 'templates/offering-edit.html'
131
def filter(self, stream, ctx):
132
return stream | HTMLFormFiller(data=ctx['data'])
134
def populate(self, req, ctx):
135
if req.method == 'POST':
136
data = dict(req.get_fieldstorage())
138
validator = OfferingSchema()
139
data = validator.to_python(data, state=req)
141
self.context.url = unicode(data['url']) if data['url'] else None
142
self.context.description = data['description']
144
req.throw_redirect(req.publisher.generate(self.context))
145
except formencode.Invalid, e:
146
errors = e.unpack_errors()
149
'url': self.context.url,
150
'description': self.context.description,
154
ctx['data'] = data or {}
155
ctx['context'] = self.context
156
ctx['errors'] = errors
58
159
class UserValidator(formencode.FancyValidator):
183
class RoleEnrolmentValidator(formencode.FancyValidator):
184
"""A FormEncode validator that checks permission to enrol users with a
187
The state must have an 'offering' attribute.
189
def _to_python(self, value, state):
190
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
191
raise formencode.Invalid('Not allowed to assign users that role',
82
196
class EnrolSchema(formencode.Schema):
83
197
user = formencode.All(NoEnrolmentValidator(), UserValidator())
198
role = formencode.All(formencode.validators.OneOf(
199
["lecturer", "tutor", "student"]),
200
RoleEnrolmentValidator(),
201
formencode.validators.UnicodeString())
204
class EnrolmentsView(XHTMLView):
205
"""A page which displays all users enrolled in an offering."""
206
template = 'templates/enrolments.html'
210
def populate(self, req, ctx):
211
ctx['offering'] = self.context
86
213
class EnrolView(XHTMLView):
87
214
"""A form to enrol a user in an offering."""
88
template = 'enrol.html'
215
template = 'templates/enrol.html'
92
def __init__(self, req, subject, year, semester):
93
"""Find the given offering by subject, year and semester."""
94
self.context = req.store.find(Offering,
95
Offering.subject_id == Subject.id,
96
Subject.short_name == subject,
97
Offering.semester_id == Semester.id,
98
Semester.year == year,
99
Semester.semester == semester).one()
104
219
def filter(self, stream, ctx):
105
220
return stream | HTMLFormFiller(data=ctx['data'])
123
238
ctx['data'] = data or {}
124
239
ctx['offering'] = self.context
240
ctx['roles_auth'] = self.context.get_permissions(req.user)
125
241
ctx['errors'] = errors
243
class OfferingProjectsView(XHTMLView):
244
"""View the projects for an offering."""
245
template = 'templates/offering_projects.html'
249
def populate(self, req, ctx):
250
self.plugin_styles[Plugin] = ["project.css"]
251
self.plugin_scripts[Plugin] = ["project.js"]
253
ctx['offering'] = self.context
254
ctx['projectsets'] = []
255
ctx['OfferingRESTView'] = OfferingRESTView
257
#Open the projectset Fragment, and render it for inclusion
258
#into the ProjectSets page
259
#XXX: This could be a lot cleaner
260
loader = genshi.template.TemplateLoader(".", auto_reload=True)
262
set_fragment = os.path.join(os.path.dirname(__file__),
263
"templates/projectset_fragment.html")
264
project_fragment = os.path.join(os.path.dirname(__file__),
265
"templates/project_fragment.html")
267
for projectset in self.context.project_sets:
268
settmpl = loader.load(set_fragment)
271
setCtx['projectset'] = projectset
272
setCtx['projects'] = []
273
setCtx['GroupsView'] = GroupsView
274
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
276
for project in projectset.projects:
277
projecttmpl = loader.load(project_fragment)
278
projectCtx = Context()
279
projectCtx['req'] = req
280
projectCtx['project'] = project
282
setCtx['projects'].append(
283
projecttmpl.generate(projectCtx))
285
ctx['projectsets'].append(settmpl.generate(setCtx))
288
class ProjectView(XHTMLView):
289
"""View the submissions for a ProjectSet"""
290
template = "templates/project.html"
294
def build_subversion_url(self, svnroot, submission):
295
princ = submission.assessed.principal
297
if isinstance(princ, User):
298
path = 'users/%s' % princ.login
300
path = 'groups/%s_%s_%s_%s' % (
301
princ.project_set.offering.subject.short_name,
302
princ.project_set.offering.semester.year,
303
princ.project_set.offering.semester.semester,
306
return urlparse.urljoin(
308
os.path.join(path, submission.path[1:] if
309
submission.path.startswith(os.sep) else
312
def populate(self, req, ctx):
313
self.plugin_styles[Plugin] = ["project.css"]
316
ctx['GroupsView'] = GroupsView
317
ctx['EnrolView'] = EnrolView
318
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
319
ctx['build_subversion_url'] = self.build_subversion_url
320
ctx['svn_addr'] = req.config['urls']['svn_addr']
321
ctx['project'] = self.context
322
ctx['user'] = req.user
128
324
class Plugin(ViewPlugin, MediaPlugin):
130
('subjects/', SubjectsView),
131
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
325
forward_routes = (root_to_subject, subject_to_offering,
326
offering_to_project, offering_to_projectset)
327
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
329
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
330
(Offering, '+index', OfferingView),
331
(Offering, '+edit', OfferingEdit),
332
(Offering, ('+enrolments', '+index'), EnrolmentsView),
333
(Offering, ('+enrolments', '+new'), EnrolView),
334
(Offering, ('+projects', '+index'), OfferingProjectsView),
335
(Project, '+index', ProjectView),
337
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
338
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
341
breadcrumbs = {Subject: SubjectBreadcrumb,
342
Offering: OfferingBreadcrumb,
343
User: UserBreadcrumb,
344
Project: ProjectBreadcrumb,
135
348
('subjects', 'Subjects',