2
# Copyright (C) 2007-2008 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22
# This is an IVLE application.
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
37
from ivle.webapp.base.xhtml import XHTMLView
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
39
from ivle.webapp import ApplicationRoot
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
ProjectSet, Project, ProjectSubmission
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
56
class SubjectsView(XHTMLView):
57
'''The view of the list of subjects.'''
58
template = 'templates/subjects.html'
61
def authorize(self, req):
62
return req.user is not None
64
def populate(self, req, ctx):
65
ctx['user'] = req.user
67
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
68
Desc(Semester.semester)):
70
# For admins, show all subjects in the system
71
offerings = list(semester.offerings.find())
73
offerings = [enrolment.offering for enrolment in
74
semester.enrolments.find(user=req.user)]
76
ctx['semesters'].append((semester, offerings))
78
class OfferingView(XHTMLView):
79
"""The home page of an offering."""
80
template = 'templates/offering.html'
84
def populate(self, req, ctx):
85
# Need the worksheet result styles.
86
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
87
ctx['context'] = self.context
89
ctx['permissions'] = self.context.get_permissions(req.user)
90
ctx['format_submission_principal'] = util.format_submission_principal
91
ctx['format_datetime'] = ivle.date.make_date_nice
92
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
93
ctx['OfferingEdit'] = OfferingEdit
95
# As we go, calculate the total score for this subject
96
# (Assessable worksheets only, mandatory problems only)
98
ctx['worksheets'], problems_total, problems_done = (
99
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
100
req.store, req.user, self.context))
102
ctx['exercises_total'] = problems_total
103
ctx['exercises_done'] = problems_done
104
if problems_total > 0:
105
if problems_done >= problems_total:
106
ctx['worksheets_complete_class'] = "complete"
107
elif problems_done > 0:
108
ctx['worksheets_complete_class'] = "semicomplete"
110
ctx['worksheets_complete_class'] = "incomplete"
111
# Calculate the final percentage and mark for the subject
112
(ctx['exercises_pct'], ctx['worksheet_mark'],
113
ctx['worksheet_max_mark']) = (
114
ivle.worksheet.utils.calculate_mark(
115
problems_done, problems_total))
118
class OfferingSchema(formencode.Schema):
119
description = formencode.validators.UnicodeString(
120
if_missing=None, not_empty=False)
121
url = formencode.validators.URL(if_missing=None, not_empty=False)
124
class OfferingEdit(XHTMLView):
125
"""A form to edit an offering's details."""
126
template = 'templates/offering-edit.html'
130
def filter(self, stream, ctx):
131
return stream | HTMLFormFiller(data=ctx['data'])
133
def populate(self, req, ctx):
134
if req.method == 'POST':
135
data = dict(req.get_fieldstorage())
137
validator = OfferingSchema()
138
data = validator.to_python(data, state=req)
140
self.context.url = unicode(data['url']) if data['url'] else None
141
self.context.description = data['description']
143
req.throw_redirect(req.publisher.generate(self.context))
144
except formencode.Invalid, e:
145
errors = e.unpack_errors()
148
'url': self.context.url,
149
'description': self.context.description,
153
ctx['data'] = data or {}
154
ctx['context'] = self.context
155
ctx['errors'] = errors
158
class UserValidator(formencode.FancyValidator):
159
"""A FormEncode validator that turns a username into a user.
161
The state must have a 'store' attribute, which is the Storm store
163
def _to_python(self, value, state):
164
user = User.get_by_login(state.store, value)
168
raise formencode.Invalid('User does not exist', value, state)
171
class NoEnrolmentValidator(formencode.FancyValidator):
172
"""A FormEncode validator that ensures absence of an enrolment.
174
The state must have an 'offering' attribute.
176
def _to_python(self, value, state):
177
if state.offering.get_enrolment(value):
178
raise formencode.Invalid('User already enrolled', value, state)
182
class RoleEnrolmentValidator(formencode.FancyValidator):
183
"""A FormEncode validator that checks permission to enrol users with a
186
The state must have an 'offering' attribute.
188
def _to_python(self, value, state):
189
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
190
raise formencode.Invalid('Not allowed to assign users that role',
195
class EnrolSchema(formencode.Schema):
196
user = formencode.All(NoEnrolmentValidator(), UserValidator())
197
role = formencode.All(formencode.validators.OneOf(
198
["lecturer", "tutor", "student"]),
199
RoleEnrolmentValidator(),
200
formencode.validators.UnicodeString())
203
class EnrolmentsView(XHTMLView):
204
"""A page which displays all users enrolled in an offering."""
205
template = 'templates/enrolments.html'
209
def populate(self, req, ctx):
210
ctx['offering'] = self.context
212
class EnrolView(XHTMLView):
213
"""A form to enrol a user in an offering."""
214
template = 'templates/enrol.html'
218
def filter(self, stream, ctx):
219
return stream | HTMLFormFiller(data=ctx['data'])
221
def populate(self, req, ctx):
222
if req.method == 'POST':
223
data = dict(req.get_fieldstorage())
225
validator = EnrolSchema()
226
req.offering = self.context # XXX: Getting into state.
227
data = validator.to_python(data, state=req)
228
self.context.enrol(data['user'], data['role'])
230
req.throw_redirect(req.uri)
231
except formencode.Invalid, e:
232
errors = e.unpack_errors()
237
ctx['data'] = data or {}
238
ctx['offering'] = self.context
239
ctx['roles_auth'] = self.context.get_permissions(req.user)
240
ctx['errors'] = errors
242
class OfferingProjectsView(XHTMLView):
243
"""View the projects for an offering."""
244
template = 'templates/offering_projects.html'
248
def populate(self, req, ctx):
249
self.plugin_styles[Plugin] = ["project.css"]
250
self.plugin_scripts[Plugin] = ["project.js"]
252
ctx['offering'] = self.context
253
ctx['projectsets'] = []
254
ctx['OfferingRESTView'] = OfferingRESTView
256
#Open the projectset Fragment, and render it for inclusion
257
#into the ProjectSets page
258
#XXX: This could be a lot cleaner
259
loader = genshi.template.TemplateLoader(".", auto_reload=True)
261
set_fragment = os.path.join(os.path.dirname(__file__),
262
"templates/projectset_fragment.html")
263
project_fragment = os.path.join(os.path.dirname(__file__),
264
"templates/project_fragment.html")
266
for projectset in self.context.project_sets:
267
settmpl = loader.load(set_fragment)
270
setCtx['projectset'] = projectset
271
setCtx['projects'] = []
272
setCtx['GroupsView'] = GroupsView
273
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
275
for project in projectset.projects:
276
projecttmpl = loader.load(project_fragment)
277
projectCtx = Context()
278
projectCtx['req'] = req
279
projectCtx['project'] = project
281
setCtx['projects'].append(
282
projecttmpl.generate(projectCtx))
284
ctx['projectsets'].append(settmpl.generate(setCtx))
287
class ProjectView(XHTMLView):
288
"""View the submissions for a ProjectSet"""
289
template = "templates/project.html"
293
def build_subversion_url(self, svnroot, submission):
294
princ = submission.assessed.principal
296
if isinstance(princ, User):
297
path = 'users/%s' % princ.login
299
path = 'groups/%s_%s_%s_%s' % (
300
princ.project_set.offering.subject.short_name,
301
princ.project_set.offering.semester.year,
302
princ.project_set.offering.semester.semester,
305
return urlparse.urljoin(
307
os.path.join(path, submission.path[1:] if
308
submission.path.startswith(os.sep) else
311
def populate(self, req, ctx):
312
self.plugin_styles[Plugin] = ["project.css"]
315
ctx['GroupsView'] = GroupsView
316
ctx['EnrolView'] = EnrolView
317
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
318
ctx['build_subversion_url'] = self.build_subversion_url
319
ctx['svn_addr'] = req.config['urls']['svn_addr']
320
ctx['project'] = self.context
321
ctx['user'] = req.user
323
class Plugin(ViewPlugin, MediaPlugin):
324
forward_routes = (root_to_subject, subject_to_offering,
325
offering_to_project, offering_to_projectset)
326
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
328
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
329
(Offering, '+index', OfferingView),
330
(Offering, '+edit', OfferingEdit),
331
(Offering, ('+enrolments', '+index'), EnrolmentsView),
332
(Offering, ('+enrolments', '+new'), EnrolView),
333
(Offering, ('+projects', '+index'), OfferingProjectsView),
334
(Project, '+index', ProjectView),
336
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
337
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
340
breadcrumbs = {Subject: SubjectBreadcrumb,
341
Offering: OfferingBreadcrumb,
342
User: UserBreadcrumb,
343
Project: ProjectBreadcrumb,
347
('subjects', 'Subjects',
348
'View subject content and complete worksheets',
349
'subjects.png', 'subjects', 5)
352
media = 'subject-media'