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
if_missing=None, not_empty=False)
153
url = formencode.validators.URL(if_missing=None, not_empty=False)
156
class OfferingEdit(XHTMLView):
157
"""A form to edit an offering's details."""
158
template = 'templates/offering-edit.html'
161
def filter(self, stream, ctx):
162
return stream | HTMLFormFiller(data=ctx['data'])
164
def populate(self, req, ctx):
165
if req.method == 'POST':
166
data = dict(req.get_fieldstorage())
168
validator = OfferingSchema()
169
data = validator.to_python(data, state=req)
171
self.context.url = unicode(data['url']) if data['url'] else None
172
self.context.description = data['description']
174
req.throw_redirect(req.publisher.generate(self.context))
175
except formencode.Invalid, e:
176
errors = e.unpack_errors()
179
'url': self.context.url,
180
'description': self.context.description,
184
ctx['data'] = data or {}
185
ctx['context'] = self.context
186
ctx['errors'] = errors
189
class UserValidator(formencode.FancyValidator):
190
"""A FormEncode validator that turns a username into a user.
192
The state must have a 'store' attribute, which is the Storm store
194
def _to_python(self, value, state):
195
user = User.get_by_login(state.store, value)
199
raise formencode.Invalid('User does not exist', value, state)
202
class NoEnrolmentValidator(formencode.FancyValidator):
203
"""A FormEncode validator that ensures absence of an enrolment.
205
The state must have an 'offering' attribute.
207
def _to_python(self, value, state):
208
if state.offering.get_enrolment(value):
209
raise formencode.Invalid('User already enrolled', value, state)
213
class RoleEnrolmentValidator(formencode.FancyValidator):
214
"""A FormEncode validator that checks permission to enrol users with a
217
The state must have an 'offering' attribute.
219
def _to_python(self, value, state):
220
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
221
raise formencode.Invalid('Not allowed to assign users that role',
226
class EnrolSchema(formencode.Schema):
227
user = formencode.All(NoEnrolmentValidator(), UserValidator())
228
role = formencode.All(formencode.validators.OneOf(
229
["lecturer", "tutor", "student"]),
230
RoleEnrolmentValidator(),
231
formencode.validators.UnicodeString())
234
class EnrolmentsView(XHTMLView):
235
"""A page which displays all users enrolled in an offering."""
236
template = 'templates/enrolments.html'
239
def populate(self, req, ctx):
240
ctx['offering'] = self.context
242
class EnrolView(XHTMLView):
243
"""A form to enrol a user in an offering."""
244
template = 'templates/enrol.html'
248
def filter(self, stream, ctx):
249
return stream | HTMLFormFiller(data=ctx['data'])
251
def populate(self, req, ctx):
252
if req.method == 'POST':
253
data = dict(req.get_fieldstorage())
255
validator = EnrolSchema()
256
req.offering = self.context # XXX: Getting into state.
257
data = validator.to_python(data, state=req)
258
self.context.enrol(data['user'], data['role'])
260
req.throw_redirect(req.uri)
261
except formencode.Invalid, e:
262
errors = e.unpack_errors()
267
ctx['data'] = data or {}
268
ctx['offering'] = self.context
269
ctx['roles_auth'] = self.context.get_permissions(req.user)
270
ctx['errors'] = errors
272
class OfferingProjectsView(XHTMLView):
273
"""View the projects for an offering."""
274
template = 'templates/offering_projects.html'
278
def populate(self, req, ctx):
279
self.plugin_styles[Plugin] = ["project.css"]
280
self.plugin_scripts[Plugin] = ["project.js"]
282
ctx['offering'] = self.context
283
ctx['projectsets'] = []
284
ctx['OfferingRESTView'] = OfferingRESTView
286
#Open the projectset Fragment, and render it for inclusion
287
#into the ProjectSets page
288
#XXX: This could be a lot cleaner
289
loader = genshi.template.TemplateLoader(".", auto_reload=True)
291
set_fragment = os.path.join(os.path.dirname(__file__),
292
"templates/projectset_fragment.html")
293
project_fragment = os.path.join(os.path.dirname(__file__),
294
"templates/project_fragment.html")
296
for projectset in self.context.project_sets:
297
settmpl = loader.load(set_fragment)
300
setCtx['projectset'] = projectset
301
setCtx['projects'] = []
302
setCtx['GroupsView'] = GroupsView
303
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
305
for project in projectset.projects:
306
projecttmpl = loader.load(project_fragment)
307
projectCtx = Context()
308
projectCtx['req'] = req
309
projectCtx['project'] = project
311
setCtx['projects'].append(
312
projecttmpl.generate(projectCtx))
314
ctx['projectsets'].append(settmpl.generate(setCtx))
317
class ProjectView(XHTMLView):
318
"""View the submissions for a ProjectSet"""
319
template = "templates/project.html"
323
def build_subversion_url(self, svnroot, submission):
324
princ = submission.assessed.principal
326
if isinstance(princ, User):
327
path = 'users/%s' % princ.login
329
path = 'groups/%s_%s_%s_%s' % (
330
princ.project_set.offering.subject.short_name,
331
princ.project_set.offering.semester.year,
332
princ.project_set.offering.semester.semester,
335
return urlparse.urljoin(
337
os.path.join(path, submission.path[1:] if
338
submission.path.startswith(os.sep) else
341
def populate(self, req, ctx):
342
self.plugin_styles[Plugin] = ["project.css"]
345
ctx['GroupsView'] = GroupsView
346
ctx['EnrolView'] = EnrolView
347
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
348
ctx['build_subversion_url'] = self.build_subversion_url
349
ctx['svn_addr'] = req.config['urls']['svn_addr']
350
ctx['project'] = self.context
351
ctx['user'] = req.user
353
class Plugin(ViewPlugin, MediaPlugin):
354
forward_routes = (root_to_subject, subject_to_offering,
355
offering_to_project, offering_to_projectset)
356
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
358
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
359
(Offering, '+index', OfferingView),
360
(Offering, '+edit', OfferingEdit),
361
(Offering, ('+enrolments', '+index'), EnrolmentsView),
362
(Offering, ('+enrolments', '+new'), EnrolView),
363
(Offering, ('+projects', '+index'), OfferingProjectsView),
364
(Project, '+index', ProjectView),
366
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
367
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
370
breadcrumbs = {Subject: SubjectBreadcrumb,
371
Offering: OfferingBreadcrumb,
372
User: UserBreadcrumb,
373
Project: ProjectBreadcrumb,
377
('subjects', 'Subjects',
378
'View subject content and complete worksheets',
379
'subjects.png', 'subjects', 5)
382
media = 'subject-media'