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'
39
appname = 'subjects' # XXX
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))
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'
129
def filter(self, stream, ctx):
130
return stream | HTMLFormFiller(data=ctx['data'])
132
def populate(self, req, ctx):
133
if req.method == 'POST':
134
data = dict(req.get_fieldstorage())
136
validator = OfferingSchema()
137
data = validator.to_python(data, state=req)
139
self.context.url = unicode(data['url']) if data['url'] else None
140
self.context.description = data['description']
142
req.throw_redirect(req.publisher.generate(self.context))
143
except formencode.Invalid, e:
144
errors = e.unpack_errors()
147
'url': self.context.url,
148
'description': self.context.description,
152
ctx['data'] = data or {}
153
ctx['context'] = self.context
154
ctx['errors'] = errors
157
class UserValidator(formencode.FancyValidator):
158
"""A FormEncode validator that turns a username into a user.
160
The state must have a 'store' attribute, which is the Storm store
162
def _to_python(self, value, state):
163
user = User.get_by_login(state.store, value)
167
raise formencode.Invalid('User does not exist', value, state)
170
class NoEnrolmentValidator(formencode.FancyValidator):
171
"""A FormEncode validator that ensures absence of an enrolment.
173
The state must have an 'offering' attribute.
175
def _to_python(self, value, state):
176
if state.offering.get_enrolment(value):
177
raise formencode.Invalid('User already enrolled', value, state)
181
class RoleEnrolmentValidator(formencode.FancyValidator):
182
"""A FormEncode validator that checks permission to enrol users with a
185
The state must have an 'offering' attribute.
187
def _to_python(self, value, state):
188
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
189
raise formencode.Invalid('Not allowed to assign users that role',
194
class EnrolSchema(formencode.Schema):
195
user = formencode.All(NoEnrolmentValidator(), UserValidator())
196
role = formencode.All(formencode.validators.OneOf(
197
["lecturer", "tutor", "student"]),
198
RoleEnrolmentValidator(),
199
formencode.validators.UnicodeString())
202
class EnrolmentsView(XHTMLView):
203
"""A page which displays all users enrolled in an offering."""
204
template = 'templates/enrolments.html'
207
def populate(self, req, ctx):
208
ctx['offering'] = self.context
210
class EnrolView(XHTMLView):
211
"""A form to enrol a user in an offering."""
212
template = 'templates/enrol.html'
216
def filter(self, stream, ctx):
217
return stream | HTMLFormFiller(data=ctx['data'])
219
def populate(self, req, ctx):
220
if req.method == 'POST':
221
data = dict(req.get_fieldstorage())
223
validator = EnrolSchema()
224
req.offering = self.context # XXX: Getting into state.
225
data = validator.to_python(data, state=req)
226
self.context.enrol(data['user'], data['role'])
228
req.throw_redirect(req.uri)
229
except formencode.Invalid, e:
230
errors = e.unpack_errors()
235
ctx['data'] = data or {}
236
ctx['offering'] = self.context
237
ctx['roles_auth'] = self.context.get_permissions(req.user)
238
ctx['errors'] = errors
240
class OfferingProjectsView(XHTMLView):
241
"""View the projects for an offering."""
242
template = 'templates/offering_projects.html'
246
def populate(self, req, ctx):
247
self.plugin_styles[Plugin] = ["project.css"]
248
self.plugin_scripts[Plugin] = ["project.js"]
250
ctx['offering'] = self.context
251
ctx['projectsets'] = []
252
ctx['OfferingRESTView'] = OfferingRESTView
254
#Open the projectset Fragment, and render it for inclusion
255
#into the ProjectSets page
256
#XXX: This could be a lot cleaner
257
loader = genshi.template.TemplateLoader(".", auto_reload=True)
259
set_fragment = os.path.join(os.path.dirname(__file__),
260
"templates/projectset_fragment.html")
261
project_fragment = os.path.join(os.path.dirname(__file__),
262
"templates/project_fragment.html")
264
for projectset in self.context.project_sets:
265
settmpl = loader.load(set_fragment)
268
setCtx['projectset'] = projectset
269
setCtx['projects'] = []
270
setCtx['GroupsView'] = GroupsView
271
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
273
for project in projectset.projects:
274
projecttmpl = loader.load(project_fragment)
275
projectCtx = Context()
276
projectCtx['req'] = req
277
projectCtx['project'] = project
279
setCtx['projects'].append(
280
projecttmpl.generate(projectCtx))
282
ctx['projectsets'].append(settmpl.generate(setCtx))
285
class ProjectView(XHTMLView):
286
"""View the submissions for a ProjectSet"""
287
template = "templates/project.html"
291
def build_subversion_url(self, svnroot, submission):
292
princ = submission.assessed.principal
294
if isinstance(princ, User):
295
path = 'users/%s' % princ.login
297
path = 'groups/%s_%s_%s_%s' % (
298
princ.project_set.offering.subject.short_name,
299
princ.project_set.offering.semester.year,
300
princ.project_set.offering.semester.semester,
303
return urlparse.urljoin(
305
os.path.join(path, submission.path[1:] if
306
submission.path.startswith(os.sep) else
309
def populate(self, req, ctx):
310
self.plugin_styles[Plugin] = ["project.css"]
313
ctx['GroupsView'] = GroupsView
314
ctx['EnrolView'] = EnrolView
315
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
316
ctx['build_subversion_url'] = self.build_subversion_url
317
ctx['svn_addr'] = req.config['urls']['svn_addr']
318
ctx['project'] = self.context
319
ctx['user'] = req.user
45
enrolled_subjects = req.user.subjects
46
unenrolled_subjects = [subject for subject in
47
req.store.find(Subject)
48
if subject not in enrolled_subjects]
50
ctx['enrolled_subjects'] = []
51
ctx['other_subjects'] = []
53
for subject in enrolled_subjects:
55
new_subj['name'] = subject.name
56
new_subj['url'] = subject.url
57
ctx['enrolled_subjects'].append(new_subj)
59
if len(unenrolled_subjects) > 0:
60
for subject in unenrolled_subjects:
62
new_subj['name'] = subject.name
63
new_subj['url'] = subject.url
64
ctx['other_subjects'].append(new_subj)
321
67
class Plugin(ViewPlugin, MediaPlugin):
322
forward_routes = (root_to_subject, subject_to_offering,
323
offering_to_project, offering_to_projectset)
324
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
326
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
327
(Offering, '+index', OfferingView),
328
(Offering, '+edit', OfferingEdit),
329
(Offering, ('+enrolments', '+index'), EnrolmentsView),
330
(Offering, ('+enrolments', '+new'), EnrolView),
331
(Offering, ('+projects', '+index'), OfferingProjectsView),
332
(Project, '+index', ProjectView),
334
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
335
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
338
breadcrumbs = {Subject: SubjectBreadcrumb,
339
Offering: OfferingBreadcrumb,
340
User: UserBreadcrumb,
341
Project: ProjectBreadcrumb,
69
('subjects/', SubjectsView),
345
('subjects', 'Subjects',
346
'View subject content and complete worksheets',
347
'subjects.png', 'subjects', 5)
73
('subjects', 'Subjects', 'Announcements and information about the '
74
'subjects you are enrolled in.', 'subjects.png', 'subjects', 5)
350
77
media = 'subject-media'