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

« back to all changes in this revision

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

  • Committer: mattgiuca
  • Date: 2008-02-15 07:16:55 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:479
userservice: svn:ignore

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.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
58
 
 
59
 
class SubjectsView(XHTMLView):
60
 
    '''The view of the list of subjects.'''
61
 
    template = 'templates/subjects.html'
62
 
    tab = 'subjects'
63
 
 
64
 
    def authorize(self, req):
65
 
        return req.user is not None
66
 
 
67
 
    def populate(self, req, ctx):
68
 
        ctx['req'] = req
69
 
        ctx['user'] = req.user
70
 
        ctx['semesters'] = []
71
 
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
72
 
        ctx['SubjectEdit'] = SubjectEdit
73
 
 
74
 
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
75
 
                                                     Desc(Semester.semester)):
76
 
            if req.user.admin:
77
 
                # For admins, show all subjects in the system
78
 
                offerings = list(semester.offerings.find())
79
 
            else:
80
 
                offerings = [enrolment.offering for enrolment in
81
 
                                    semester.enrolments.find(user=req.user)]
82
 
            if len(offerings):
83
 
                ctx['semesters'].append((semester, offerings))
84
 
 
85
 
        # Admins get a separate list of subjects so they can add/edit.
86
 
        if req.user.admin:
87
 
            ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
88
 
 
89
 
 
90
 
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
91
 
    """A FormEncode validator that checks that a subject name is unused.
92
 
 
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.
95
 
    """
96
 
    def __init__(self, matching=None):
97
 
        self.matching = matching
98
 
 
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)
105
 
        return value
106
 
 
107
 
 
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)
114
 
 
115
 
 
116
 
class SubjectFormView(XHTMLView):
117
 
    """An abstract form to add or edit a subject."""
118
 
    tab = 'subjects'
119
 
 
120
 
    def authorize(self, req):
121
 
        return req.user is not None and req.user.admin
122
 
 
123
 
    def filter(self, stream, ctx):
124
 
        return stream | HTMLFormFiller(data=ctx['data'])
125
 
 
126
 
    def populate_state(self, state):
127
 
        state.existing_subject = None
128
 
 
129
 
    def populate(self, req, ctx):
130
 
        if req.method == 'POST':
131
 
            data = dict(req.get_fieldstorage())
132
 
            try:
133
 
                validator = SubjectSchema()
134
 
                self.populate_state(req)
135
 
                data = validator.to_python(data, state=req)
136
 
 
137
 
                subject = self.update_subject_object(req, data)
138
 
 
139
 
                req.store.commit()
140
 
                req.throw_redirect(req.publisher.generate(subject))
141
 
            except formencode.Invalid, e:
142
 
                errors = e.unpack_errors()
143
 
        else:
144
 
            data = self.get_default_data(req)
145
 
            errors = {}
146
 
 
147
 
        if errors:
148
 
            req.store.rollback()
149
 
 
150
 
        ctx['context'] = self.context
151
 
        ctx['data'] = data or {}
152
 
        ctx['errors'] = errors
153
 
 
154
 
 
155
 
class SubjectNew(SubjectFormView):
156
 
    """A form to create a subject."""
157
 
    template = 'templates/subject-new.html'
158
 
 
159
 
    def populate_state(self, state):
160
 
        state.existing_subject = self.context
161
 
 
162
 
    def get_default_data(self, req):
163
 
        return {}
164
 
 
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']
170
 
 
171
 
        req.store.add(new_subject)
172
 
        return new_subject
173
 
 
174
 
 
175
 
class SubjectEdit(SubjectFormView):
176
 
    """A form to edit a subject."""
177
 
    template = 'templates/subject-edit.html'
178
 
 
179
 
    def populate_state(self, state):
180
 
        state.existing_subject = self.context
181
 
 
182
 
    def get_default_data(self, req):
183
 
        return {
184
 
            'short_name': self.context.short_name,
185
 
            'name': self.context.name,
186
 
            'code': self.context.code,
187
 
            }
188
 
 
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']
193
 
 
194
 
        return self.context
195
 
 
196
 
 
197
 
class OfferingView(XHTMLView):
198
 
    """The home page of an offering."""
199
 
    template = 'templates/offering.html'
200
 
    tab = 'subjects'
201
 
    permission = 'view'
202
 
 
203
 
    def populate(self, req, ctx):
204
 
        # Need the worksheet result styles.
205
 
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
206
 
        ctx['context'] = self.context
207
 
        ctx['req'] = req
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
213
 
 
214
 
        # As we go, calculate the total score for this subject
215
 
        # (Assessable worksheets only, mandatory problems only)
216
 
 
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))
220
 
 
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"
228
 
            else:
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))
235
 
 
236
 
 
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)
241
 
 
242
 
 
243
 
class OfferingEdit(XHTMLView):
244
 
    """A form to edit an offering's details."""
245
 
    template = 'templates/offering-edit.html'
246
 
    tab = 'subjects'
247
 
    permission = 'edit'
248
 
 
249
 
    def filter(self, stream, ctx):
250
 
        return stream | HTMLFormFiller(data=ctx['data'])
251
 
 
252
 
    def populate(self, req, ctx):
253
 
        if req.method == 'POST':
254
 
            data = dict(req.get_fieldstorage())
255
 
            try:
256
 
                validator = OfferingSchema()
257
 
                data = validator.to_python(data, state=req)
258
 
 
259
 
                self.context.url = unicode(data['url']) if data['url'] else None
260
 
                self.context.description = data['description']
261
 
                req.store.commit()
262
 
                req.throw_redirect(req.publisher.generate(self.context))
263
 
            except formencode.Invalid, e:
264
 
                errors = e.unpack_errors()
265
 
        else:
266
 
            data = {
267
 
                'url': self.context.url,
268
 
                'description': self.context.description,
269
 
            }
270
 
            errors = {}
271
 
 
272
 
        ctx['data'] = data or {}
273
 
        ctx['context'] = self.context
274
 
        ctx['errors'] = errors
275
 
 
276
 
 
277
 
class UserValidator(formencode.FancyValidator):
278
 
    """A FormEncode validator that turns a username into a user.
279
 
 
280
 
    The state must have a 'store' attribute, which is the Storm store
281
 
    to use."""
282
 
    def _to_python(self, value, state):
283
 
        user = User.get_by_login(state.store, value)
284
 
        if user:
285
 
            return user
286
 
        else:
287
 
            raise formencode.Invalid('User does not exist', value, state)
288
 
 
289
 
 
290
 
class NoEnrolmentValidator(formencode.FancyValidator):
291
 
    """A FormEncode validator that ensures absence of an enrolment.
292
 
 
293
 
    The state must have an 'offering' attribute.
294
 
    """
295
 
    def _to_python(self, value, state):
296
 
        if state.offering.get_enrolment(value):
297
 
            raise formencode.Invalid('User already enrolled', value, state)
298
 
        return value
299
 
 
300
 
 
301
 
class RoleEnrolmentValidator(formencode.FancyValidator):
302
 
    """A FormEncode validator that checks permission to enrol users with a
303
 
    particular role.
304
 
 
305
 
    The state must have an 'offering' attribute.
306
 
    """
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',
310
 
                                     value, state)
311
 
        return value
312
 
 
313
 
 
314
 
class EnrolSchema(formencode.Schema):
315
 
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
316
 
    role = formencode.All(formencode.validators.OneOf(
317
 
                                ["lecturer", "tutor", "student"]),
318
 
                          RoleEnrolmentValidator(),
319
 
                          formencode.validators.UnicodeString())
320
 
 
321
 
 
322
 
class EnrolmentsView(XHTMLView):
323
 
    """A page which displays all users enrolled in an offering."""
324
 
    template = 'templates/enrolments.html'
325
 
    tab = 'subjects'
326
 
    permission = 'edit'
327
 
 
328
 
    def populate(self, req, ctx):
329
 
        ctx['offering'] = self.context
330
 
 
331
 
class EnrolView(XHTMLView):
332
 
    """A form to enrol a user in an offering."""
333
 
    template = 'templates/enrol.html'
334
 
    tab = 'subjects'
335
 
    permission = 'enrol'
336
 
 
337
 
    def filter(self, stream, ctx):
338
 
        return stream | HTMLFormFiller(data=ctx['data'])
339
 
 
340
 
    def populate(self, req, ctx):
341
 
        if req.method == 'POST':
342
 
            data = dict(req.get_fieldstorage())
343
 
            try:
344
 
                validator = EnrolSchema()
345
 
                req.offering = self.context # XXX: Getting into state.
346
 
                data = validator.to_python(data, state=req)
347
 
                self.context.enrol(data['user'], data['role'])
348
 
                req.store.commit()
349
 
                req.throw_redirect(req.uri)
350
 
            except formencode.Invalid, e:
351
 
                errors = e.unpack_errors()
352
 
        else:
353
 
            data = {}
354
 
            errors = {}
355
 
 
356
 
        ctx['data'] = data or {}
357
 
        ctx['offering'] = self.context
358
 
        ctx['roles_auth'] = self.context.get_permissions(req.user)
359
 
        ctx['errors'] = errors
360
 
 
361
 
class OfferingProjectsView(XHTMLView):
362
 
    """View the projects for an offering."""
363
 
    template = 'templates/offering_projects.html'
364
 
    permission = 'edit'
365
 
    tab = 'subjects'
366
 
 
367
 
    def populate(self, req, ctx):
368
 
        self.plugin_styles[Plugin] = ["project.css"]
369
 
        self.plugin_scripts[Plugin] = ["project.js"]
370
 
        ctx['req'] = req
371
 
        ctx['offering'] = self.context
372
 
        ctx['projectsets'] = []
373
 
        ctx['OfferingRESTView'] = OfferingRESTView
374
 
 
375
 
        #Open the projectset Fragment, and render it for inclusion
376
 
        #into the ProjectSets page
377
 
        #XXX: This could be a lot cleaner
378
 
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
379
 
 
380
 
        set_fragment = os.path.join(os.path.dirname(__file__),
381
 
                "templates/projectset_fragment.html")
382
 
        project_fragment = os.path.join(os.path.dirname(__file__),
383
 
                "templates/project_fragment.html")
384
 
 
385
 
        for projectset in self.context.project_sets:
386
 
            settmpl = loader.load(set_fragment)
387
 
            setCtx = Context()
388
 
            setCtx['req'] = req
389
 
            setCtx['projectset'] = projectset
390
 
            setCtx['projects'] = []
391
 
            setCtx['GroupsView'] = GroupsView
392
 
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
393
 
 
394
 
            for project in projectset.projects:
395
 
                projecttmpl = loader.load(project_fragment)
396
 
                projectCtx = Context()
397
 
                projectCtx['req'] = req
398
 
                projectCtx['project'] = project
399
 
 
400
 
                setCtx['projects'].append(
401
 
                        projecttmpl.generate(projectCtx))
402
 
 
403
 
            ctx['projectsets'].append(settmpl.generate(setCtx))
404
 
 
405
 
 
406
 
class ProjectView(XHTMLView):
407
 
    """View the submissions for a ProjectSet"""
408
 
    template = "templates/project.html"
409
 
    permission = "edit"
410
 
    tab = 'subjects'
411
 
 
412
 
    def build_subversion_url(self, svnroot, submission):
413
 
        princ = submission.assessed.principal
414
 
 
415
 
        if isinstance(princ, User):
416
 
            path = 'users/%s' % princ.login
417
 
        else:
418
 
            path = 'groups/%s_%s_%s_%s' % (
419
 
                    princ.project_set.offering.subject.short_name,
420
 
                    princ.project_set.offering.semester.year,
421
 
                    princ.project_set.offering.semester.semester,
422
 
                    princ.name
423
 
                    )
424
 
        return urlparse.urljoin(
425
 
                    svnroot,
426
 
                    os.path.join(path, submission.path[1:] if
427
 
                                       submission.path.startswith(os.sep) else
428
 
                                       submission.path))
429
 
 
430
 
    def populate(self, req, ctx):
431
 
        self.plugin_styles[Plugin] = ["project.css"]
432
 
 
433
 
        ctx['req'] = req
434
 
        ctx['GroupsView'] = GroupsView
435
 
        ctx['EnrolView'] = EnrolView
436
 
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
437
 
        ctx['build_subversion_url'] = self.build_subversion_url
438
 
        ctx['svn_addr'] = req.config['urls']['svn_addr']
439
 
        ctx['project'] = self.context
440
 
        ctx['user'] = req.user
441
 
 
442
 
class Plugin(ViewPlugin, MediaPlugin):
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)
446
 
 
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),
456
 
 
457
 
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
458
 
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
459
 
             ]
460
 
 
461
 
    breadcrumbs = {Subject: SubjectBreadcrumb,
462
 
                   Offering: OfferingBreadcrumb,
463
 
                   User: UserBreadcrumb,
464
 
                   Project: ProjectBreadcrumb,
465
 
                   }
466
 
 
467
 
    tabs = [
468
 
        ('subjects', 'Subjects',
469
 
         'View subject content and complete worksheets',
470
 
         'subjects.png', 'subjects', 5)
471
 
    ]
472
 
 
473
 
    media = 'subject-media'