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))
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)
110
class OfferingView(XHTMLView):
111
"""The home page of an offering."""
112
template = 'templates/offering.html'
116
def populate(self, req, ctx):
117
# Need the worksheet result styles.
118
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
119
ctx['context'] = self.context
121
ctx['permissions'] = self.context.get_permissions(req.user)
122
ctx['format_submission_principal'] = format_submission_principal
123
ctx['format_datetime'] = ivle.date.make_date_nice
124
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
125
ctx['OfferingEdit'] = OfferingEdit
127
# As we go, calculate the total score for this subject
128
# (Assessable worksheets only, mandatory problems only)
130
ctx['worksheets'], problems_total, problems_done = (
131
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
132
req.store, req.user, self.context))
134
ctx['exercises_total'] = problems_total
135
ctx['exercises_done'] = problems_done
136
if problems_total > 0:
137
if problems_done >= problems_total:
138
ctx['worksheets_complete_class'] = "complete"
139
elif problems_done > 0:
140
ctx['worksheets_complete_class'] = "semicomplete"
142
ctx['worksheets_complete_class'] = "incomplete"
143
# Calculate the final percentage and mark for the subject
144
(ctx['exercises_pct'], ctx['worksheet_mark'],
145
ctx['worksheet_max_mark']) = (
146
ivle.worksheet.utils.calculate_mark(
147
problems_done, problems_total))
150
class OfferingSchema(formencode.Schema):
151
description = formencode.validators.UnicodeString()
152
url = formencode.validators.URL()
155
class OfferingEdit(XHTMLView):
156
"""A form to edit an offering's details."""
157
template = 'templates/offering-edit.html'
160
def filter(self, stream, ctx):
161
return stream | HTMLFormFiller(data=ctx['data'])
163
def populate(self, req, ctx):
164
if req.method == 'POST':
165
data = dict(req.get_fieldstorage())
167
validator = OfferingSchema()
168
data = validator.to_python(data, state=req)
170
self.context.url = unicode(data['url'])
171
self.context.description = data['description']
173
req.throw_redirect(req.publisher.generate(self.context))
174
except formencode.Invalid, e:
175
errors = e.unpack_errors()
178
'url': self.context.url,
179
'description': self.context.description,
183
ctx['data'] = data or {}
184
ctx['context'] = self.context
185
ctx['errors'] = errors
188
class UserValidator(formencode.FancyValidator):
189
"""A FormEncode validator that turns a username into a user.
191
The state must have a 'store' attribute, which is the Storm store
193
def _to_python(self, value, state):
194
user = User.get_by_login(state.store, value)
198
raise formencode.Invalid('User does not exist', value, state)
201
class NoEnrolmentValidator(formencode.FancyValidator):
202
"""A FormEncode validator that ensures absence of an enrolment.
204
The state must have an 'offering' attribute.
206
def _to_python(self, value, state):
207
if state.offering.get_enrolment(value):
208
raise formencode.Invalid('User already enrolled', value, state)
212
class RoleEnrolmentValidator(formencode.FancyValidator):
213
"""A FormEncode validator that checks permission to enrol users with a
216
The state must have an 'offering' attribute.
218
def _to_python(self, value, state):
219
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
220
raise formencode.Invalid('Not allowed to assign users that role',
225
class EnrolSchema(formencode.Schema):
226
user = formencode.All(NoEnrolmentValidator(), UserValidator())
227
role = formencode.All(formencode.validators.OneOf(
228
["lecturer", "tutor", "student"]),
229
RoleEnrolmentValidator(),
230
formencode.validators.UnicodeString())
233
class EnrolmentsView(XHTMLView):
234
"""A page which displays all users enrolled in an offering."""
235
template = 'templates/enrolments.html'
238
def populate(self, req, ctx):
239
ctx['offering'] = self.context
241
class EnrolView(XHTMLView):
242
"""A form to enrol a user in an offering."""
243
template = 'templates/enrol.html'
247
def filter(self, stream, ctx):
248
return stream | HTMLFormFiller(data=ctx['data'])
250
def populate(self, req, ctx):
251
if req.method == 'POST':
252
data = dict(req.get_fieldstorage())
254
validator = EnrolSchema()
255
req.offering = self.context # XXX: Getting into state.
256
data = validator.to_python(data, state=req)
257
self.context.enrol(data['user'], data['role'])
259
req.throw_redirect(req.uri)
260
except formencode.Invalid, e:
261
errors = e.unpack_errors()
266
ctx['data'] = data or {}
267
ctx['offering'] = self.context
268
ctx['roles_auth'] = self.context.get_permissions(req.user)
269
ctx['errors'] = errors
271
class OfferingProjectsView(XHTMLView):
272
"""View the projects for an offering."""
273
template = 'templates/offering_projects.html'
277
def populate(self, req, ctx):
278
self.plugin_styles[Plugin] = ["project.css"]
279
self.plugin_scripts[Plugin] = ["project.js"]
281
ctx['offering'] = self.context
282
ctx['projectsets'] = []
283
ctx['OfferingRESTView'] = OfferingRESTView
285
#Open the projectset Fragment, and render it for inclusion
286
#into the ProjectSets page
287
#XXX: This could be a lot cleaner
288
loader = genshi.template.TemplateLoader(".", auto_reload=True)
290
set_fragment = os.path.join(os.path.dirname(__file__),
291
"templates/projectset_fragment.html")
292
project_fragment = os.path.join(os.path.dirname(__file__),
293
"templates/project_fragment.html")
295
for projectset in self.context.project_sets:
296
settmpl = loader.load(set_fragment)
299
setCtx['projectset'] = projectset
300
setCtx['projects'] = []
301
setCtx['GroupsView'] = GroupsView
302
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
304
for project in projectset.projects:
305
projecttmpl = loader.load(project_fragment)
306
projectCtx = Context()
307
projectCtx['req'] = req
308
projectCtx['project'] = project
310
setCtx['projects'].append(
311
projecttmpl.generate(projectCtx))
313
ctx['projectsets'].append(settmpl.generate(setCtx))
316
class ProjectView(XHTMLView):
317
"""View the submissions for a ProjectSet"""
318
template = "templates/project.html"
322
def build_subversion_url(self, svnroot, submission):
323
princ = submission.assessed.principal
325
if isinstance(princ, User):
326
path = 'users/%s' % princ.login
328
path = 'groups/%s_%s_%s_%s' % (
329
princ.project_set.offering.subject.short_name,
330
princ.project_set.offering.semester.year,
331
princ.project_set.offering.semester.semester,
334
return urlparse.urljoin(
336
os.path.join(path, submission.path[1:] if
337
submission.path.startswith(os.sep) else
340
def populate(self, req, ctx):
341
self.plugin_styles[Plugin] = ["project.css"]
344
ctx['GroupsView'] = GroupsView
345
ctx['EnrolView'] = EnrolView
346
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
347
ctx['build_subversion_url'] = self.build_subversion_url
348
ctx['svn_addr'] = req.config['urls']['svn_addr']
349
ctx['project'] = self.context
350
ctx['user'] = req.user
352
class Plugin(ViewPlugin, MediaPlugin):
353
forward_routes = (root_to_subject, subject_to_offering,
354
offering_to_project, offering_to_projectset)
355
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
357
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
358
(Offering, '+index', OfferingView),
359
(Offering, '+edit', OfferingEdit),
360
(Offering, ('+enrolments', '+index'), EnrolmentsView),
361
(Offering, ('+enrolments', '+new'), EnrolView),
362
(Offering, ('+projects', '+index'), OfferingProjectsView),
363
(Project, '+index', ProjectView),
365
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
366
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
369
breadcrumbs = {Subject: SubjectBreadcrumb,
370
Offering: OfferingBreadcrumb,
371
User: UserBreadcrumb,
372
Project: ProjectBreadcrumb,
376
('subjects', 'Subjects',
377
'View subject content and complete worksheets',
378
'subjects.png', 'subjects', 5)
381
media = 'subject-media'