37
37
from ivle.webapp.base.xhtml import XHTMLView
38
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
39
from ivle.webapp import ApplicationRoot
39
from ivle.webapp.errors import NotFound
41
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
42
ProjectSet, Project, ProjectSubmission
43
43
from ivle import util
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
47
48
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
51
class SubjectsView(XHTMLView):
57
52
'''The view of the list of subjects.'''
66
61
ctx['semesters'] = []
67
62
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
68
63
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'
130
def filter(self, stream, ctx):
131
return stream | HTMLFormFiller(data=ctx['data'])
133
def populate(self, req, ctx):
134
if req.method == 'POST':
135
data = dict(req.get_fieldstorage())
137
validator = OfferingSchema()
138
data = validator.to_python(data, state=req)
140
self.context.url = unicode(data['url']) if data['url'] else None
141
self.context.description = data['description']
143
req.throw_redirect(req.publisher.generate(self.context))
144
except formencode.Invalid, e:
145
errors = e.unpack_errors()
148
'url': self.context.url,
149
'description': self.context.description,
153
ctx['data'] = data or {}
154
ctx['context'] = self.context
155
ctx['errors'] = errors
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
158
69
class UserValidator(formencode.FancyValidator):
182
class RoleEnrolmentValidator(formencode.FancyValidator):
183
"""A FormEncode validator that checks permission to enrol users with a
186
The state must have an 'offering' attribute.
188
def _to_python(self, value, state):
189
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
190
raise formencode.Invalid('Not allowed to assign users that role',
195
93
class EnrolSchema(formencode.Schema):
196
94
user = formencode.All(NoEnrolmentValidator(), UserValidator())
197
role = formencode.All(formencode.validators.OneOf(
198
["lecturer", "tutor", "student"]),
199
RoleEnrolmentValidator(),
200
formencode.validators.UnicodeString())
203
class EnrolmentsView(XHTMLView):
204
"""A page which displays all users enrolled in an offering."""
205
template = 'templates/enrolments.html'
209
def populate(self, req, ctx):
210
ctx['offering'] = self.context
212
97
class EnrolView(XHTMLView):
213
98
"""A form to enrol a user in an offering."""
214
99
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()
218
115
def filter(self, stream, ctx):
219
116
return stream | HTMLFormFiller(data=ctx['data'])
244
140
template = 'templates/offering_projects.html'
245
141
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"
248
169
def populate(self, req, ctx):
249
170
self.plugin_styles[Plugin] = ["project.css"]
250
171
self.plugin_scripts[Plugin] = ["project.js"]
252
172
ctx['offering'] = self.context
253
173
ctx['projectsets'] = []
254
ctx['OfferingRESTView'] = OfferingRESTView
256
175
#Open the projectset Fragment, and render it for inclusion
257
176
#into the ProjectSets page
266
185
for projectset in self.context.project_sets:
267
186
settmpl = loader.load(set_fragment)
268
187
setCtx = Context()
270
188
setCtx['projectset'] = projectset
189
setCtx['new_project_url'] = self.new_project_url(projectset)
271
190
setCtx['projects'] = []
272
setCtx['GroupsView'] = GroupsView
273
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
275
192
for project in projectset.projects:
276
193
projecttmpl = loader.load(project_fragment)
277
194
projectCtx = Context()
278
projectCtx['req'] = req
279
195
projectCtx['project'] = project
196
projectCtx['project_url'] = self.project_url(projectset, project)
281
198
setCtx['projects'].append(
282
199
projecttmpl.generate(projectCtx))
290
207
permission = "edit"
210
def __init__(self, req, subject, year, semester, project):
211
self.context = req.store.find(Project,
212
Project.short_name == project,
213
Project.project_set_id == ProjectSet.id,
214
ProjectSet.offering_id == Offering.id,
215
Offering.semester_id == Semester.id,
216
Semester.year == year,
217
Semester.semester == semester,
218
Offering.subject_id == Subject.id,
219
Subject.short_name == subject).one()
220
if self.context is None:
293
223
def build_subversion_url(self, svnroot, submission):
294
224
princ = submission.assessed.principal
321
248
ctx['user'] = req.user
323
250
class Plugin(ViewPlugin, MediaPlugin):
324
forward_routes = (root_to_subject, subject_to_offering,
325
offering_to_project, offering_to_projectset)
326
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
328
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
329
(Offering, '+index', OfferingView),
330
(Offering, '+edit', OfferingEdit),
331
(Offering, ('+enrolments', '+index'), EnrolmentsView),
332
(Offering, ('+enrolments', '+new'), EnrolView),
333
(Offering, ('+projects', '+index'), OfferingProjectsView),
334
(Project, '+index', ProjectView),
336
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
337
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
340
breadcrumbs = {Subject: SubjectBreadcrumb,
341
Offering: OfferingBreadcrumb,
342
User: UserBreadcrumb,
343
Project: ProjectBreadcrumb,
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',
347
267
('subjects', 'Subjects',