31
from storm.locals import Desc
31
from storm.locals import Desc, Store
33
33
from genshi.filters import HTMLFormFiller
34
34
from genshi.template import Context, TemplateLoader
36
import formencode.validators
37
38
from ivle.webapp.base.xhtml import XHTMLView
38
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
39
from ivle.webapp.errors import NotFound
40
from ivle.webapp import ApplicationRoot
41
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
43
ProjectSet, Project, ProjectSubmission
43
44
from ivle import util
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
47
from ivle.webapp.admin.projectservice import ProjectSetRESTView
48
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
49
from ivle.webapp.admin.publishing import (root_to_subject,
50
subject_to_offering, offering_to_projectset, offering_to_project,
51
subject_url, offering_url, projectset_url, project_url)
52
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
53
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
54
from ivle.webapp.core import Plugin as CorePlugin
55
from ivle.webapp.groups import GroupsView
56
from ivle.webapp.media import media_url
57
from ivle.webapp.tutorial import Plugin as TutorialPlugin
51
59
class SubjectsView(XHTMLView):
52
60
'''The view of the list of subjects.'''
57
65
return req.user is not None
59
67
def populate(self, req, ctx):
60
69
ctx['user'] = req.user
61
70
ctx['semesters'] = []
71
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
72
ctx['SubjectEdit'] = SubjectEdit
62
74
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
63
75
Desc(Semester.semester)):
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
77
# For admins, show all subjects in the system
78
offerings = list(semester.offerings.find())
80
offerings = [enrolment.offering for enrolment in
81
semester.enrolments.find(user=req.user)]
83
ctx['semesters'].append((semester, offerings))
85
# Admins get a separate list of subjects so they can add/edit.
87
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
90
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
91
"""A FormEncode validator that checks that a subject name is unused.
93
The subject referenced by state.existing_subject is permitted
94
to hold that name. If any other object holds it, the input is rejected.
96
def __init__(self, matching=None):
97
self.matching = matching
99
def _to_python(self, value, state):
100
if (state.store.find(
101
Subject, short_name=value).one() not in
102
(None, state.existing_subject)):
103
raise formencode.Invalid(
104
'Short name already taken', value, state)
108
class SubjectSchema(formencode.Schema):
109
short_name = formencode.All(
110
SubjectShortNameUniquenessValidator(),
111
formencode.validators.UnicodeString(not_empty=True))
112
name = formencode.validators.UnicodeString(not_empty=True)
113
code = formencode.validators.UnicodeString(not_empty=True)
116
class SubjectFormView(XHTMLView):
117
"""An abstract form to add or edit a subject."""
120
def authorize(self, req):
121
return req.user is not None and req.user.admin
123
def filter(self, stream, ctx):
124
return stream | HTMLFormFiller(data=ctx['data'])
126
def populate_state(self, state):
127
state.existing_subject = None
129
def populate(self, req, ctx):
130
if req.method == 'POST':
131
data = dict(req.get_fieldstorage())
133
validator = SubjectSchema()
134
self.populate_state(req)
135
data = validator.to_python(data, state=req)
137
subject = self.update_subject_object(req, data)
140
req.throw_redirect(req.publisher.generate(subject))
141
except formencode.Invalid, e:
142
errors = e.unpack_errors()
144
data = self.get_default_data(req)
150
ctx['context'] = self.context
151
ctx['data'] = data or {}
152
ctx['errors'] = errors
155
class SubjectNew(SubjectFormView):
156
"""A form to create a subject."""
157
template = 'templates/subject-new.html'
159
def populate_state(self, state):
160
state.existing_subject = self.context
162
def get_default_data(self, req):
165
def update_subject_object(self, req, data):
166
new_subject = Subject()
167
new_subject.short_name = data['short_name']
168
new_subject.name = data['name']
169
new_subject.code = data['code']
171
req.store.add(new_subject)
175
class SubjectEdit(SubjectFormView):
176
"""A form to edit a subject."""
177
template = 'templates/subject-edit.html'
179
def populate_state(self, state):
180
state.existing_subject = self.context
182
def get_default_data(self, req):
184
'short_name': self.context.short_name,
185
'name': self.context.name,
186
'code': self.context.code,
189
def update_subject_object(self, req, data):
190
self.context.short_name = data['short_name']
191
self.context.name = data['name']
192
self.context.code = data['code']
197
class OfferingView(XHTMLView):
198
"""The home page of an offering."""
199
template = 'templates/offering.html'
203
def populate(self, req, ctx):
204
# Need the worksheet result styles.
205
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
206
ctx['context'] = self.context
208
ctx['permissions'] = self.context.get_permissions(req.user)
209
ctx['format_submission_principal'] = util.format_submission_principal
210
ctx['format_datetime'] = ivle.date.make_date_nice
211
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
212
ctx['OfferingEdit'] = OfferingEdit
214
# As we go, calculate the total score for this subject
215
# (Assessable worksheets only, mandatory problems only)
217
ctx['worksheets'], problems_total, problems_done = (
218
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
219
req.store, req.user, self.context))
221
ctx['exercises_total'] = problems_total
222
ctx['exercises_done'] = problems_done
223
if problems_total > 0:
224
if problems_done >= problems_total:
225
ctx['worksheets_complete_class'] = "complete"
226
elif problems_done > 0:
227
ctx['worksheets_complete_class'] = "semicomplete"
229
ctx['worksheets_complete_class'] = "incomplete"
230
# Calculate the final percentage and mark for the subject
231
(ctx['exercises_pct'], ctx['worksheet_mark'],
232
ctx['worksheet_max_mark']) = (
233
ivle.worksheet.utils.calculate_mark(
234
problems_done, problems_total))
237
class OfferingSchema(formencode.Schema):
238
description = formencode.validators.UnicodeString(
239
if_missing=None, not_empty=False)
240
url = formencode.validators.URL(if_missing=None, not_empty=False)
243
class OfferingEdit(XHTMLView):
244
"""A form to edit an offering's details."""
245
template = 'templates/offering-edit.html'
249
def filter(self, stream, ctx):
250
return stream | HTMLFormFiller(data=ctx['data'])
252
def populate(self, req, ctx):
253
if req.method == 'POST':
254
data = dict(req.get_fieldstorage())
256
validator = OfferingSchema()
257
data = validator.to_python(data, state=req)
259
self.context.url = unicode(data['url']) if data['url'] else None
260
self.context.description = data['description']
262
req.throw_redirect(req.publisher.generate(self.context))
263
except formencode.Invalid, e:
264
errors = e.unpack_errors()
267
'url': self.context.url,
268
'description': self.context.description,
272
ctx['data'] = data or {}
273
ctx['context'] = self.context
274
ctx['errors'] = errors
69
277
class UserValidator(formencode.FancyValidator):
301
class RoleEnrolmentValidator(formencode.FancyValidator):
302
"""A FormEncode validator that checks permission to enrol users with a
305
The state must have an 'offering' attribute.
307
def _to_python(self, value, state):
308
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
309
raise formencode.Invalid('Not allowed to assign users that role',
93
314
class EnrolSchema(formencode.Schema):
94
315
user = formencode.All(NoEnrolmentValidator(), UserValidator())
316
role = formencode.All(formencode.validators.OneOf(
317
["lecturer", "tutor", "student"]),
318
RoleEnrolmentValidator(),
319
formencode.validators.UnicodeString())
322
class EnrolmentsView(XHTMLView):
323
"""A page which displays all users enrolled in an offering."""
324
template = 'templates/enrolments.html'
328
def populate(self, req, ctx):
329
ctx['offering'] = self.context
97
331
class EnrolView(XHTMLView):
98
332
"""A form to enrol a user in an offering."""
99
333
template = 'templates/enrol.html'
103
def __init__(self, req, subject, year, semester):
104
"""Find the given offering by subject, year and semester."""
105
self.context = req.store.find(Offering,
106
Offering.subject_id == Subject.id,
107
Subject.short_name == subject,
108
Offering.semester_id == Semester.id,
109
Semester.year == year,
110
Semester.semester == semester).one()
115
337
def filter(self, stream, ctx):
116
338
return stream | HTMLFormFiller(data=ctx['data'])
140
363
template = 'templates/offering_projects.html'
141
364
permission = 'edit'
144
def __init__(self, req, subject, year, semester):
145
self.context = req.store.find(Offering,
146
Offering.subject_id == Subject.id,
147
Subject.short_name == subject,
148
Offering.semester_id == Semester.id,
149
Semester.year == year,
150
Semester.semester == semester).one()
155
def project_url(self, projectset, project):
156
return "/subjects/%s/%s/%s/+projects/%s" % (
157
self.context.subject.short_name,
158
self.context.semester.year,
159
self.context.semester.semester,
163
def new_project_url(self, projectset):
164
return "/api/subjects/" + self.context.subject.short_name + "/" +\
165
self.context.semester.year + "/" + \
166
self.context.semester.semester + "/+projectsets/" +\
167
str(projectset.id) + "/+projects/+new"
169
367
def populate(self, req, ctx):
170
368
self.plugin_styles[Plugin] = ["project.css"]
171
369
self.plugin_scripts[Plugin] = ["project.js"]
172
371
ctx['offering'] = self.context
173
372
ctx['projectsets'] = []
373
ctx['OfferingRESTView'] = OfferingRESTView
175
375
#Open the projectset Fragment, and render it for inclusion
176
376
#into the ProjectSets page
248
440
ctx['user'] = req.user
250
442
class Plugin(ViewPlugin, MediaPlugin):
252
('subjects/', SubjectsView),
253
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
254
('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
255
('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
257
('api/subjects/:subject/:year/:semester/+projectsets/+new',
259
('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
261
('api/subjects/:subject/:year/:semester/+projects/:project',
443
forward_routes = (root_to_subject, subject_to_offering,
444
offering_to_project, offering_to_projectset)
445
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
447
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
448
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
449
(Subject, '+edit', SubjectEdit),
450
(Offering, '+index', OfferingView),
451
(Offering, '+edit', OfferingEdit),
452
(Offering, ('+enrolments', '+index'), EnrolmentsView),
453
(Offering, ('+enrolments', '+new'), EnrolView),
454
(Offering, ('+projects', '+index'), OfferingProjectsView),
455
(Project, '+index', ProjectView),
457
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
458
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
461
breadcrumbs = {Subject: SubjectBreadcrumb,
462
Offering: OfferingBreadcrumb,
463
User: UserBreadcrumb,
464
Project: ProjectBreadcrumb,
267
468
('subjects', 'Subjects',