~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/webapp/admin/subject.py

  • Committer: William Grant
  • Date: 2010-02-11 11:33:33 UTC
  • Revision ID: grantw@unimelb.edu.au-20100211113333-0j040ct188bfpjl5
Add subject creation/editing UI. Not linked just yet.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
# App: subjects
 
19
# Author: Matt Giuca
 
20
# Date: 29/2/2008
 
21
 
 
22
# This is an IVLE application.
 
23
# A sample / testing application for IVLE.
 
24
 
 
25
import os
 
26
import os.path
 
27
import urllib
 
28
import urlparse
 
29
import cgi
 
30
 
 
31
from storm.locals import Desc, Store
 
32
import genshi
 
33
from genshi.filters import HTMLFormFiller
 
34
from genshi.template import Context, TemplateLoader
 
35
import formencode
 
36
import formencode.validators
 
37
 
 
38
from ivle.webapp.base.xhtml import XHTMLView
 
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
40
from ivle.webapp import ApplicationRoot
 
41
 
 
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
43
                          ProjectSet, Project, ProjectSubmission
 
44
from ivle import util
 
45
import ivle.date
 
46
 
 
47
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
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.groups import GroupsView
 
55
from ivle.webapp.tutorial import Plugin as TutorialPlugin
 
56
 
 
57
class SubjectsView(XHTMLView):
 
58
    '''The view of the list of subjects.'''
 
59
    template = 'templates/subjects.html'
 
60
    tab = 'subjects'
 
61
 
 
62
    def authorize(self, req):
 
63
        return req.user is not None
 
64
 
 
65
    def populate(self, req, ctx):
 
66
        ctx['req'] = req
 
67
        ctx['user'] = req.user
 
68
        ctx['semesters'] = []
 
69
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
 
70
                                                     Desc(Semester.semester)):
 
71
            if req.user.admin:
 
72
                # For admins, show all subjects in the system
 
73
                offerings = list(semester.offerings.find())
 
74
            else:
 
75
                offerings = [enrolment.offering for enrolment in
 
76
                                    semester.enrolments.find(user=req.user)]
 
77
            if len(offerings):
 
78
                ctx['semesters'].append((semester, offerings))
 
79
 
 
80
 
 
81
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
82
    """A FormEncode validator that checks that a subject name is unused.
 
83
 
 
84
    The subject referenced by state.existing_subject is permitted
 
85
    to hold that name. If any other object holds it, the input is rejected.
 
86
    """
 
87
    def __init__(self, matching=None):
 
88
        self.matching = matching
 
89
 
 
90
    def _to_python(self, value, state):
 
91
        if (state.store.find(
 
92
                Subject, short_name=value).one() not in
 
93
                (None, state.existing_subject)):
 
94
            raise formencode.Invalid(
 
95
                'Short name already taken', value, state)
 
96
        return value
 
97
 
 
98
 
 
99
class SubjectSchema(formencode.Schema):
 
100
    short_name = formencode.All(
 
101
        SubjectShortNameUniquenessValidator(),
 
102
        formencode.validators.UnicodeString(not_empty=True))
 
103
    name = formencode.validators.UnicodeString(not_empty=True)
 
104
    code = formencode.validators.UnicodeString(not_empty=True)
 
105
 
 
106
 
 
107
class SubjectFormView(XHTMLView):
 
108
    """An abstract form to add or edit a subject."""
 
109
    tab = 'subjects'
 
110
 
 
111
    def authorize(self, req):
 
112
        return req.user is not None and req.user.admin
 
113
 
 
114
    def filter(self, stream, ctx):
 
115
        return stream | HTMLFormFiller(data=ctx['data'])
 
116
 
 
117
    def populate_state(self, state):
 
118
        state.existing_subject = None
 
119
 
 
120
    def populate(self, req, ctx):
 
121
        if req.method == 'POST':
 
122
            data = dict(req.get_fieldstorage())
 
123
            try:
 
124
                validator = SubjectSchema()
 
125
                self.populate_state(req)
 
126
                data = validator.to_python(data, state=req)
 
127
 
 
128
                subject = self.update_subject_object(req, data)
 
129
 
 
130
                req.store.commit()
 
131
                req.throw_redirect(req.publisher.generate(subject))
 
132
            except formencode.Invalid, e:
 
133
                errors = e.unpack_errors()
 
134
        else:
 
135
            data = self.get_default_data(req)
 
136
            errors = {}
 
137
 
 
138
        if errors:
 
139
            req.store.rollback()
 
140
 
 
141
        ctx['context'] = self.context
 
142
        ctx['data'] = data or {}
 
143
        ctx['errors'] = errors
 
144
 
 
145
 
 
146
class SubjectNew(SubjectFormView):
 
147
    """A form to create a subject."""
 
148
    template = 'templates/subject-new.html'
 
149
 
 
150
    def populate_state(self, state):
 
151
        state.existing_subject = self.context
 
152
 
 
153
    def get_default_data(self, req):
 
154
        return {}
 
155
 
 
156
    def update_subject_object(self, req, data):
 
157
        new_subject = Subject()
 
158
        new_subject.short_name = data['short_name']
 
159
        new_subject.name = data['name']
 
160
        new_subject.code = data['code']
 
161
 
 
162
        req.store.add(new_subject)
 
163
        return new_subject
 
164
 
 
165
 
 
166
class SubjectEdit(SubjectFormView):
 
167
    """A form to edit a subject."""
 
168
    template = 'templates/subject-edit.html'
 
169
 
 
170
    def populate_state(self, state):
 
171
        state.existing_subject = self.context
 
172
 
 
173
    def get_default_data(self, req):
 
174
        return {
 
175
            'short_name': self.context.short_name,
 
176
            'name': self.context.name,
 
177
            'code': self.context.code,
 
178
            }
 
179
 
 
180
    def update_subject_object(self, req, data):
 
181
        self.context.short_name = data['short_name']
 
182
        self.context.name = data['name']
 
183
        self.context.code = data['code']
 
184
 
 
185
        return self.context
 
186
 
 
187
 
 
188
class OfferingView(XHTMLView):
 
189
    """The home page of an offering."""
 
190
    template = 'templates/offering.html'
 
191
    tab = 'subjects'
 
192
    permission = 'view'
 
193
 
 
194
    def populate(self, req, ctx):
 
195
        # Need the worksheet result styles.
 
196
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
197
        ctx['context'] = self.context
 
198
        ctx['req'] = req
 
199
        ctx['permissions'] = self.context.get_permissions(req.user)
 
200
        ctx['format_submission_principal'] = util.format_submission_principal
 
201
        ctx['format_datetime'] = ivle.date.make_date_nice
 
202
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
203
        ctx['OfferingEdit'] = OfferingEdit
 
204
 
 
205
        # As we go, calculate the total score for this subject
 
206
        # (Assessable worksheets only, mandatory problems only)
 
207
 
 
208
        ctx['worksheets'], problems_total, problems_done = (
 
209
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
210
                req.store, req.user, self.context))
 
211
 
 
212
        ctx['exercises_total'] = problems_total
 
213
        ctx['exercises_done'] = problems_done
 
214
        if problems_total > 0:
 
215
            if problems_done >= problems_total:
 
216
                ctx['worksheets_complete_class'] = "complete"
 
217
            elif problems_done > 0:
 
218
                ctx['worksheets_complete_class'] = "semicomplete"
 
219
            else:
 
220
                ctx['worksheets_complete_class'] = "incomplete"
 
221
            # Calculate the final percentage and mark for the subject
 
222
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
223
             ctx['worksheet_max_mark']) = (
 
224
                ivle.worksheet.utils.calculate_mark(
 
225
                    problems_done, problems_total))
 
226
 
 
227
 
 
228
class OfferingSchema(formencode.Schema):
 
229
    description = formencode.validators.UnicodeString(
 
230
        if_missing=None, not_empty=False)
 
231
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
232
 
 
233
 
 
234
class OfferingEdit(XHTMLView):
 
235
    """A form to edit an offering's details."""
 
236
    template = 'templates/offering-edit.html'
 
237
    tab = 'subjects'
 
238
    permission = 'edit'
 
239
 
 
240
    def filter(self, stream, ctx):
 
241
        return stream | HTMLFormFiller(data=ctx['data'])
 
242
 
 
243
    def populate(self, req, ctx):
 
244
        if req.method == 'POST':
 
245
            data = dict(req.get_fieldstorage())
 
246
            try:
 
247
                validator = OfferingSchema()
 
248
                data = validator.to_python(data, state=req)
 
249
 
 
250
                self.context.url = unicode(data['url']) if data['url'] else None
 
251
                self.context.description = data['description']
 
252
                req.store.commit()
 
253
                req.throw_redirect(req.publisher.generate(self.context))
 
254
            except formencode.Invalid, e:
 
255
                errors = e.unpack_errors()
 
256
        else:
 
257
            data = {
 
258
                'url': self.context.url,
 
259
                'description': self.context.description,
 
260
            }
 
261
            errors = {}
 
262
 
 
263
        ctx['data'] = data or {}
 
264
        ctx['context'] = self.context
 
265
        ctx['errors'] = errors
 
266
 
 
267
 
 
268
class UserValidator(formencode.FancyValidator):
 
269
    """A FormEncode validator that turns a username into a user.
 
270
 
 
271
    The state must have a 'store' attribute, which is the Storm store
 
272
    to use."""
 
273
    def _to_python(self, value, state):
 
274
        user = User.get_by_login(state.store, value)
 
275
        if user:
 
276
            return user
 
277
        else:
 
278
            raise formencode.Invalid('User does not exist', value, state)
 
279
 
 
280
 
 
281
class NoEnrolmentValidator(formencode.FancyValidator):
 
282
    """A FormEncode validator that ensures absence of an enrolment.
 
283
 
 
284
    The state must have an 'offering' attribute.
 
285
    """
 
286
    def _to_python(self, value, state):
 
287
        if state.offering.get_enrolment(value):
 
288
            raise formencode.Invalid('User already enrolled', value, state)
 
289
        return value
 
290
 
 
291
 
 
292
class RoleEnrolmentValidator(formencode.FancyValidator):
 
293
    """A FormEncode validator that checks permission to enrol users with a
 
294
    particular role.
 
295
 
 
296
    The state must have an 'offering' attribute.
 
297
    """
 
298
    def _to_python(self, value, state):
 
299
        if ("enrol_" + value) not in state.offering.get_permissions(state.user):
 
300
            raise formencode.Invalid('Not allowed to assign users that role',
 
301
                                     value, state)
 
302
        return value
 
303
 
 
304
 
 
305
class EnrolSchema(formencode.Schema):
 
306
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
307
    role = formencode.All(formencode.validators.OneOf(
 
308
                                ["lecturer", "tutor", "student"]),
 
309
                          RoleEnrolmentValidator(),
 
310
                          formencode.validators.UnicodeString())
 
311
 
 
312
 
 
313
class EnrolmentsView(XHTMLView):
 
314
    """A page which displays all users enrolled in an offering."""
 
315
    template = 'templates/enrolments.html'
 
316
    tab = 'subjects'
 
317
    permission = 'edit'
 
318
 
 
319
    def populate(self, req, ctx):
 
320
        ctx['offering'] = self.context
 
321
 
 
322
class EnrolView(XHTMLView):
 
323
    """A form to enrol a user in an offering."""
 
324
    template = 'templates/enrol.html'
 
325
    tab = 'subjects'
 
326
    permission = 'enrol'
 
327
 
 
328
    def filter(self, stream, ctx):
 
329
        return stream | HTMLFormFiller(data=ctx['data'])
 
330
 
 
331
    def populate(self, req, ctx):
 
332
        if req.method == 'POST':
 
333
            data = dict(req.get_fieldstorage())
 
334
            try:
 
335
                validator = EnrolSchema()
 
336
                req.offering = self.context # XXX: Getting into state.
 
337
                data = validator.to_python(data, state=req)
 
338
                self.context.enrol(data['user'], data['role'])
 
339
                req.store.commit()
 
340
                req.throw_redirect(req.uri)
 
341
            except formencode.Invalid, e:
 
342
                errors = e.unpack_errors()
 
343
        else:
 
344
            data = {}
 
345
            errors = {}
 
346
 
 
347
        ctx['data'] = data or {}
 
348
        ctx['offering'] = self.context
 
349
        ctx['roles_auth'] = self.context.get_permissions(req.user)
 
350
        ctx['errors'] = errors
 
351
 
 
352
class OfferingProjectsView(XHTMLView):
 
353
    """View the projects for an offering."""
 
354
    template = 'templates/offering_projects.html'
 
355
    permission = 'edit'
 
356
    tab = 'subjects'
 
357
 
 
358
    def populate(self, req, ctx):
 
359
        self.plugin_styles[Plugin] = ["project.css"]
 
360
        self.plugin_scripts[Plugin] = ["project.js"]
 
361
        ctx['req'] = req
 
362
        ctx['offering'] = self.context
 
363
        ctx['projectsets'] = []
 
364
        ctx['OfferingRESTView'] = OfferingRESTView
 
365
 
 
366
        #Open the projectset Fragment, and render it for inclusion
 
367
        #into the ProjectSets page
 
368
        #XXX: This could be a lot cleaner
 
369
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
370
 
 
371
        set_fragment = os.path.join(os.path.dirname(__file__),
 
372
                "templates/projectset_fragment.html")
 
373
        project_fragment = os.path.join(os.path.dirname(__file__),
 
374
                "templates/project_fragment.html")
 
375
 
 
376
        for projectset in self.context.project_sets:
 
377
            settmpl = loader.load(set_fragment)
 
378
            setCtx = Context()
 
379
            setCtx['req'] = req
 
380
            setCtx['projectset'] = projectset
 
381
            setCtx['projects'] = []
 
382
            setCtx['GroupsView'] = GroupsView
 
383
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
384
 
 
385
            for project in projectset.projects:
 
386
                projecttmpl = loader.load(project_fragment)
 
387
                projectCtx = Context()
 
388
                projectCtx['req'] = req
 
389
                projectCtx['project'] = project
 
390
 
 
391
                setCtx['projects'].append(
 
392
                        projecttmpl.generate(projectCtx))
 
393
 
 
394
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
395
 
 
396
 
 
397
class ProjectView(XHTMLView):
 
398
    """View the submissions for a ProjectSet"""
 
399
    template = "templates/project.html"
 
400
    permission = "edit"
 
401
    tab = 'subjects'
 
402
 
 
403
    def build_subversion_url(self, svnroot, submission):
 
404
        princ = submission.assessed.principal
 
405
 
 
406
        if isinstance(princ, User):
 
407
            path = 'users/%s' % princ.login
 
408
        else:
 
409
            path = 'groups/%s_%s_%s_%s' % (
 
410
                    princ.project_set.offering.subject.short_name,
 
411
                    princ.project_set.offering.semester.year,
 
412
                    princ.project_set.offering.semester.semester,
 
413
                    princ.name
 
414
                    )
 
415
        return urlparse.urljoin(
 
416
                    svnroot,
 
417
                    os.path.join(path, submission.path[1:] if
 
418
                                       submission.path.startswith(os.sep) else
 
419
                                       submission.path))
 
420
 
 
421
    def populate(self, req, ctx):
 
422
        self.plugin_styles[Plugin] = ["project.css"]
 
423
 
 
424
        ctx['req'] = req
 
425
        ctx['GroupsView'] = GroupsView
 
426
        ctx['EnrolView'] = EnrolView
 
427
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
428
        ctx['build_subversion_url'] = self.build_subversion_url
 
429
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
430
        ctx['project'] = self.context
 
431
        ctx['user'] = req.user
 
432
 
 
433
class Plugin(ViewPlugin, MediaPlugin):
 
434
    forward_routes = (root_to_subject, subject_to_offering,
 
435
                      offering_to_project, offering_to_projectset)
 
436
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
437
 
 
438
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
439
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
440
             (Subject, '+edit', SubjectEdit),
 
441
             (Offering, '+index', OfferingView),
 
442
             (Offering, '+edit', OfferingEdit),
 
443
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
444
             (Offering, ('+enrolments', '+new'), EnrolView),
 
445
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
446
             (Project, '+index', ProjectView),
 
447
 
 
448
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
449
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
450
             ]
 
451
 
 
452
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
453
                   Offering: OfferingBreadcrumb,
 
454
                   User: UserBreadcrumb,
 
455
                   Project: ProjectBreadcrumb,
 
456
                   }
 
457
 
 
458
    tabs = [
 
459
        ('subjects', 'Subjects',
 
460
         'View subject content and complete worksheets',
 
461
         'subjects.png', 'subjects', 5)
 
462
    ]
 
463
 
 
464
    media = 'subject-media'