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
37
29
from ivle.webapp.base.xhtml import XHTMLView
38
30
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
31
from ivle.webapp.errors import NotFound
32
from ivle.database import Subject
43
33
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
56
36
class SubjectsView(XHTMLView):
57
37
'''The view of the list of subjects.'''
58
template = 'templates/subjects.html'
38
template = 'subjects.html'
61
41
def authorize(self, req):
62
42
return req.user is not None
64
44
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
126
# As we go, calculate the total score for this subject
127
# (Assessable worksheets only, mandatory problems only)
129
ctx['worksheets'], problems_total, problems_done = (
130
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
131
req.store, req.user, self.context))
133
ctx['exercises_total'] = problems_total
134
ctx['exercises_done'] = problems_done
135
if problems_total > 0:
136
if problems_done >= problems_total:
137
ctx['worksheets_complete_class'] = "complete"
138
elif problems_done > 0:
139
ctx['worksheets_complete_class'] = "semicomplete"
141
ctx['worksheets_complete_class'] = "incomplete"
142
# Calculate the final percentage and mark for the subject
143
(ctx['exercises_pct'], ctx['worksheet_mark'],
144
ctx['worksheet_max_mark']) = (
145
ivle.worksheet.utils.calculate_mark(
146
problems_done, problems_total))
149
class UserValidator(formencode.FancyValidator):
150
"""A FormEncode validator that turns a username into a user.
152
The state must have a 'store' attribute, which is the Storm store
154
def _to_python(self, value, state):
155
user = User.get_by_login(state.store, value)
159
raise formencode.Invalid('User does not exist', value, state)
162
class NoEnrolmentValidator(formencode.FancyValidator):
163
"""A FormEncode validator that ensures absence of an enrolment.
165
The state must have an 'offering' attribute.
167
def _to_python(self, value, state):
168
if state.offering.get_enrolment(value):
169
raise formencode.Invalid('User already enrolled', value, state)
173
class RoleEnrolmentValidator(formencode.FancyValidator):
174
"""A FormEncode validator that checks permission to enrol users with a
177
The state must have an 'offering' attribute.
179
def _to_python(self, value, state):
180
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
181
raise formencode.Invalid('Not allowed to assign users that role',
186
class EnrolSchema(formencode.Schema):
187
user = formencode.All(NoEnrolmentValidator(), UserValidator())
188
role = formencode.All(formencode.validators.OneOf(
189
["lecturer", "tutor", "student"]),
190
RoleEnrolmentValidator(),
191
formencode.validators.UnicodeString())
194
class EnrolmentsView(XHTMLView):
195
"""A page which displays all users enrolled in an offering."""
196
template = 'templates/enrolments.html'
199
def populate(self, req, ctx):
200
ctx['offering'] = self.context
202
class EnrolView(XHTMLView):
203
"""A form to enrol a user in an offering."""
204
template = 'templates/enrol.html'
208
def filter(self, stream, ctx):
209
return stream | HTMLFormFiller(data=ctx['data'])
211
def populate(self, req, ctx):
212
if req.method == 'POST':
213
data = dict(req.get_fieldstorage())
215
validator = EnrolSchema()
216
req.offering = self.context # XXX: Getting into state.
217
data = validator.to_python(data, state=req)
218
self.context.enrol(data['user'], data['role'])
220
req.throw_redirect(req.uri)
221
except formencode.Invalid, e:
222
errors = e.unpack_errors()
227
ctx['data'] = data or {}
228
ctx['offering'] = self.context
229
ctx['roles_auth'] = self.context.get_permissions(req.user)
230
ctx['errors'] = errors
232
class OfferingProjectsView(XHTMLView):
233
"""View the projects for an offering."""
234
template = 'templates/offering_projects.html'
238
def populate(self, req, ctx):
239
self.plugin_styles[Plugin] = ["project.css"]
240
self.plugin_scripts[Plugin] = ["project.js"]
242
ctx['offering'] = self.context
243
ctx['projectsets'] = []
244
ctx['OfferingRESTView'] = OfferingRESTView
246
#Open the projectset Fragment, and render it for inclusion
247
#into the ProjectSets page
248
#XXX: This could be a lot cleaner
249
loader = genshi.template.TemplateLoader(".", auto_reload=True)
251
set_fragment = os.path.join(os.path.dirname(__file__),
252
"templates/projectset_fragment.html")
253
project_fragment = os.path.join(os.path.dirname(__file__),
254
"templates/project_fragment.html")
256
for projectset in self.context.project_sets:
257
settmpl = loader.load(set_fragment)
260
setCtx['projectset'] = projectset
261
setCtx['projects'] = []
262
setCtx['GroupsView'] = GroupsView
263
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
265
for project in projectset.projects:
266
projecttmpl = loader.load(project_fragment)
267
projectCtx = Context()
268
projectCtx['req'] = req
269
projectCtx['project'] = project
271
setCtx['projects'].append(
272
projecttmpl.generate(projectCtx))
274
ctx['projectsets'].append(settmpl.generate(setCtx))
277
class ProjectView(XHTMLView):
278
"""View the submissions for a ProjectSet"""
279
template = "templates/project.html"
283
def build_subversion_url(self, svnroot, submission):
284
princ = submission.assessed.principal
286
if isinstance(princ, User):
287
path = 'users/%s' % princ.login
289
path = 'groups/%s_%s_%s_%s' % (
290
princ.project_set.offering.subject.short_name,
291
princ.project_set.offering.semester.year,
292
princ.project_set.offering.semester.semester,
295
return urlparse.urljoin(
297
os.path.join(path, submission.path[1:] if
298
submission.path.startswith(os.sep) else
301
def populate(self, req, ctx):
302
self.plugin_styles[Plugin] = ["project.css"]
305
ctx['GroupsView'] = GroupsView
306
ctx['EnrolView'] = EnrolView
307
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
308
ctx['build_subversion_url'] = self.build_subversion_url
309
ctx['svn_addr'] = req.config['urls']['svn_addr']
310
ctx['project'] = self.context
311
ctx['user'] = req.user
45
ctx['enrolments'] = req.user.active_enrolments
313
47
class Plugin(ViewPlugin, MediaPlugin):
314
forward_routes = (root_to_subject, subject_to_offering,
315
offering_to_project, offering_to_projectset)
316
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
318
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
319
(Offering, '+index', OfferingView),
320
(Offering, ('+enrolments', '+index'), EnrolmentsView),
321
(Offering, ('+enrolments', '+new'), EnrolView),
322
(Offering, ('+projects', '+index'), OfferingProjectsView),
323
(Project, '+index', ProjectView),
325
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
326
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
329
breadcrumbs = {Subject: SubjectBreadcrumb,
330
Offering: OfferingBreadcrumb,
331
User: UserBreadcrumb,
332
Project: ProjectBreadcrumb,
49
('subjects/', SubjectsView),
336
53
('subjects', 'Subjects',