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.errors import NotFound
40
from ivle.webapp import ApplicationRoot
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
43
ProjectSet, Project, ProjectSubmission
47
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
49
from ivle.webapp.admin.offeringservice import OfferingRESTView
51
class SubjectsView(XHTMLView):
52
'''The view of the list of subjects.'''
53
template = 'templates/subjects.html'
56
def authorize(self, req):
57
return req.user is not None
59
def populate(self, req, ctx):
60
ctx['user'] = req.user
62
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
63
Desc(Semester.semester)):
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
69
class UserValidator(formencode.FancyValidator):
70
"""A FormEncode validator that turns a username into a user.
72
The state must have a 'store' attribute, which is the Storm store
74
def _to_python(self, value, state):
75
user = User.get_by_login(state.store, value)
79
raise formencode.Invalid('User does not exist', value, state)
82
class NoEnrolmentValidator(formencode.FancyValidator):
83
"""A FormEncode validator that ensures absence of an enrolment.
85
The state must have an 'offering' attribute.
87
def _to_python(self, value, state):
88
if state.offering.get_enrolment(value):
89
raise formencode.Invalid('User already enrolled', value, state)
93
class EnrolSchema(formencode.Schema):
94
user = formencode.All(NoEnrolmentValidator(), UserValidator())
97
class EnrolView(XHTMLView):
98
"""A form to enrol a user in an offering."""
99
template = 'templates/enrol.html'
103
def filter(self, stream, ctx):
104
return stream | HTMLFormFiller(data=ctx['data'])
106
def populate(self, req, ctx):
107
if req.method == 'POST':
108
data = dict(req.get_fieldstorage())
110
validator = EnrolSchema()
111
req.offering = self.context # XXX: Getting into state.
112
data = validator.to_python(data, state=req)
113
self.context.enrol(data['user'])
115
req.throw_redirect(req.uri)
116
except formencode.Invalid, e:
117
errors = e.unpack_errors()
122
ctx['data'] = data or {}
123
ctx['offering'] = self.context
124
ctx['errors'] = errors
126
class OfferingProjectsView(XHTMLView):
127
"""View the projects for an offering."""
128
template = 'templates/offering_projects.html'
132
def project_url(self, projectset, project):
133
return "/subjects/%s/%s/%s/+projects/%s" % (
134
self.context.subject.short_name,
135
self.context.semester.year,
136
self.context.semester.semester,
140
def new_project_url(self, projectset):
141
return "/api/subjects/" + self.context.subject.short_name + "/" +\
142
self.context.semester.year + "/" + \
143
self.context.semester.semester + "/+projectsets/" +\
144
str(projectset.id) + "/+projects/+new"
146
def populate(self, req, ctx):
147
self.plugin_styles[Plugin] = ["project.css"]
148
self.plugin_scripts[Plugin] = ["project.js"]
149
ctx['offering'] = self.context
150
ctx['projectsets'] = []
152
#Open the projectset Fragment, and render it for inclusion
153
#into the ProjectSets page
154
#XXX: This could be a lot cleaner
155
loader = genshi.template.TemplateLoader(".", auto_reload=True)
157
set_fragment = os.path.join(os.path.dirname(__file__),
158
"templates/projectset_fragment.html")
159
project_fragment = os.path.join(os.path.dirname(__file__),
160
"templates/project_fragment.html")
162
for projectset in self.context.project_sets:
163
settmpl = loader.load(set_fragment)
165
setCtx['projectset'] = projectset
166
setCtx['new_project_url'] = self.new_project_url(projectset)
167
setCtx['projects'] = []
169
for project in projectset.projects:
170
projecttmpl = loader.load(project_fragment)
171
projectCtx = Context()
172
projectCtx['project'] = project
173
projectCtx['project_url'] = self.project_url(projectset, project)
175
setCtx['projects'].append(
176
projecttmpl.generate(projectCtx))
178
ctx['projectsets'].append(settmpl.generate(setCtx))
181
class ProjectView(XHTMLView):
182
"""View the submissions for a ProjectSet"""
183
template = "templates/project.html"
187
def build_subversion_url(self, svnroot, submission):
188
princ = submission.assessed.principal
190
if isinstance(princ, User):
191
path = 'users/%s' % princ.login
193
path = 'groups/%s_%s_%s_%s' % (
194
princ.project_set.offering.subject.short_name,
195
princ.project_set.offering.semester.year,
196
princ.project_set.offering.semester.semester,
199
return urlparse.urljoin(
201
os.path.join(path, submission.path[1:] if
202
submission.path.startswith(os.sep) else
205
def populate(self, req, ctx):
206
self.plugin_styles[Plugin] = ["project.css"]
208
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
209
ctx['build_subversion_url'] = self.build_subversion_url
210
ctx['svn_addr'] = req.config['urls']['svn_addr']
211
ctx['project'] = self.context
212
ctx['user'] = req.user
214
class OfferingEnrolmentSet(object):
215
def __init__(self, offering):
216
self.offering = offering
218
def root_to_subject(root, name):
219
return root.store.find(Subject, short_name=name).one()
221
def subject_to_offering(subject, year, semester):
222
return subject.offering_for_semester(year, semester)
224
def offering_to_project(offering, name):
225
return Store.of(offering).find(Project,
226
Project.project_set_id == ProjectSet.id,
227
ProjectSet.offering == offering).one()
229
def offering_to_projectset(offering, name):
230
return Store.of(offering).find(ProjectSet,
231
ProjectSet.offering == offering).one()
233
class Plugin(ViewPlugin, MediaPlugin):
234
forward_routes = [(ApplicationRoot, 'subjects', root_to_subject, 1),
235
(Subject, None, subject_to_offering, 2),
236
(Offering, '+projects', offering_to_project, 1),
237
(Offering, '+projectsets', offering_to_projectset, 1),
240
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
241
(Offering, ('+enrolments', '+new'), EnrolView),
242
(Offering, ('+projects', '+index'), OfferingProjectsView),
243
(Project, '+index', ProjectView),
245
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
246
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
247
(Project, '+index', ProjectRESTView, 'api'),
251
('subjects', 'Subjects',
252
'View subject content and complete worksheets',
253
'subjects.png', 'subjects', 5)
256
media = 'subject-media'