~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-15 01:12:46 UTC
  • Revision ID: grantw@unimelb.edu.au-20100215011246-2kxz9ro0gnrvqueo
Document how to tag releases in bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
# A sample / testing application for IVLE.
24
24
 
25
25
import os
 
26
import os.path
26
27
import urllib
 
28
import urlparse
27
29
import cgi
28
30
 
29
 
from storm.locals import Desc
 
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
30
37
 
 
38
from ivle.webapp.base.forms import BaseFormView
 
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
40
from ivle.webapp.base.xhtml import XHTMLView
32
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
33
 
from ivle.webapp.errors import NotFound
34
 
from ivle.database import Subject, Semester
 
41
from ivle.webapp import ApplicationRoot
 
42
 
 
43
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
44
                          ProjectSet, Project, ProjectSubmission
35
45
from ivle import util
 
46
import ivle.date
36
47
 
 
48
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
49
from ivle.webapp.admin.offeringservice import OfferingRESTView
 
50
from ivle.webapp.admin.publishing import (root_to_subject,
 
51
            subject_to_offering, offering_to_projectset, offering_to_project,
 
52
            subject_url, offering_url, projectset_url, project_url)
 
53
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
 
54
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
55
from ivle.webapp.core import Plugin as CorePlugin
 
56
from ivle.webapp.groups import GroupsView
 
57
from ivle.webapp.media import media_url
 
58
from ivle.webapp.tutorial import Plugin as TutorialPlugin
37
59
 
38
60
class SubjectsView(XHTMLView):
39
61
    '''The view of the list of subjects.'''
40
 
    template = 'subjects.html'
 
62
    template = 'templates/subjects.html'
41
63
    tab = 'subjects'
42
64
 
43
65
    def authorize(self, req):
44
66
        return req.user is not None
45
67
 
46
68
    def populate(self, req, ctx):
 
69
        ctx['req'] = req
 
70
        ctx['user'] = req.user
47
71
        ctx['semesters'] = []
 
72
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
73
        ctx['SubjectEdit'] = SubjectEdit
 
74
 
48
75
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
49
76
                                                     Desc(Semester.semester)):
50
 
            enrolments = semester.enrolments.find(user=req.user)
51
 
            if enrolments.count():
52
 
                ctx['semesters'].append((semester, enrolments))
 
77
            if req.user.admin:
 
78
                # For admins, show all subjects in the system
 
79
                offerings = list(semester.offerings.find())
 
80
            else:
 
81
                offerings = [enrolment.offering for enrolment in
 
82
                                    semester.enrolments.find(user=req.user)]
 
83
            if len(offerings):
 
84
                ctx['semesters'].append((semester, offerings))
 
85
 
 
86
        # Admins get a separate list of subjects so they can add/edit.
 
87
        if req.user.admin:
 
88
            ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
89
 
 
90
 
 
91
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
92
    """A FormEncode validator that checks that a subject name is unused.
 
93
 
 
94
    The subject referenced by state.existing_subject is permitted
 
95
    to hold that name. If any other object holds it, the input is rejected.
 
96
    """
 
97
    def __init__(self, matching=None):
 
98
        self.matching = matching
 
99
 
 
100
    def _to_python(self, value, state):
 
101
        if (state.store.find(
 
102
                Subject, short_name=value).one() not in
 
103
                (None, state.existing_subject)):
 
104
            raise formencode.Invalid(
 
105
                'Short name already taken', value, state)
 
106
        return value
 
107
 
 
108
 
 
109
class SubjectSchema(formencode.Schema):
 
110
    short_name = formencode.All(
 
111
        SubjectShortNameUniquenessValidator(),
 
112
        formencode.validators.UnicodeString(not_empty=True))
 
113
    name = formencode.validators.UnicodeString(not_empty=True)
 
114
    code = formencode.validators.UnicodeString(not_empty=True)
 
115
 
 
116
 
 
117
class SubjectFormView(BaseFormView):
 
118
    """An abstract form to add or edit a subject."""
 
119
    tab = 'subjects'
 
120
 
 
121
    def authorize(self, req):
 
122
        return req.user is not None and req.user.admin
 
123
 
 
124
    def populate_state(self, state):
 
125
        state.existing_subject = None
 
126
 
 
127
    @property
 
128
    def validator(self):
 
129
        return SubjectSchema()
 
130
 
 
131
    def get_return_url(self, obj):
 
132
        return '/subjects'
 
133
 
 
134
 
 
135
class SubjectNew(SubjectFormView):
 
136
    """A form to create a subject."""
 
137
    template = 'templates/subject-new.html'
 
138
 
 
139
    def get_default_data(self, req):
 
140
        return {}
 
141
 
 
142
    def save_object(self, req, data):
 
143
        new_subject = Subject()
 
144
        new_subject.short_name = data['short_name']
 
145
        new_subject.name = data['name']
 
146
        new_subject.code = data['code']
 
147
 
 
148
        req.store.add(new_subject)
 
149
        return new_subject
 
150
 
 
151
 
 
152
class SubjectEdit(SubjectFormView):
 
153
    """A form to edit a subject."""
 
154
    template = 'templates/subject-edit.html'
 
155
 
 
156
    def populate_state(self, state):
 
157
        state.existing_subject = self.context
 
158
 
 
159
    def get_default_data(self, req):
 
160
        return {
 
161
            'short_name': self.context.short_name,
 
162
            'name': self.context.name,
 
163
            'code': self.context.code,
 
164
            }
 
165
 
 
166
    def save_object(self, req, data):
 
167
        self.context.short_name = data['short_name']
 
168
        self.context.name = data['name']
 
169
        self.context.code = data['code']
 
170
 
 
171
        return self.context
 
172
 
 
173
 
 
174
class SemesterUniquenessValidator(formencode.FancyValidator):
 
175
    """A FormEncode validator that checks that a semester is unique.
 
176
 
 
177
    There cannot be more than one semester for the same year and semester.
 
178
    """
 
179
    def _to_python(self, value, state):
 
180
        if (state.store.find(
 
181
                Semester, year=value['year'], semester=value['semester']
 
182
                ).count() > 0):
 
183
            raise formencode.Invalid(
 
184
                'Semester already exists', value, state)
 
185
        return value
 
186
 
 
187
 
 
188
class SemesterSchema(formencode.Schema):
 
189
    year = formencode.validators.UnicodeString()
 
190
    semester = formencode.validators.UnicodeString()
 
191
    chained_validators = [SemesterUniquenessValidator()]
 
192
 
 
193
 
 
194
class SemesterNew(BaseFormView):
 
195
    """A form to create a semester."""
 
196
    template = 'templates/semester-new.html'
 
197
    tab = 'subjects'
 
198
 
 
199
    def authorize(self, req):
 
200
        return req.user is not None and req.user.admin
 
201
 
 
202
    @property
 
203
    def validator(self):
 
204
        return SemesterSchema()
 
205
 
 
206
    def get_default_data(self, req):
 
207
        return {}
 
208
 
 
209
    def save_object(self, req, data):
 
210
        new_semester = Semester()
 
211
        new_semester.year = data['year']
 
212
        new_semester.semester = data['semester']
 
213
 
 
214
        req.store.add(new_semester)
 
215
        return new_semester
 
216
 
 
217
    def get_return_url(self, obj):
 
218
        return '/subjects'
 
219
 
 
220
 
 
221
class OfferingView(XHTMLView):
 
222
    """The home page of an offering."""
 
223
    template = 'templates/offering.html'
 
224
    tab = 'subjects'
 
225
    permission = 'view'
 
226
 
 
227
    def populate(self, req, ctx):
 
228
        # Need the worksheet result styles.
 
229
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
230
        ctx['context'] = self.context
 
231
        ctx['req'] = req
 
232
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
233
        ctx['format_submission_principal'] = util.format_submission_principal
 
234
        ctx['format_datetime'] = ivle.date.make_date_nice
 
235
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
236
        ctx['OfferingEdit'] = OfferingEdit
 
237
        ctx['GroupsView'] = GroupsView
 
238
 
 
239
        # As we go, calculate the total score for this subject
 
240
        # (Assessable worksheets only, mandatory problems only)
 
241
 
 
242
        ctx['worksheets'], problems_total, problems_done = (
 
243
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
244
                req.store, req.user, self.context))
 
245
 
 
246
        ctx['exercises_total'] = problems_total
 
247
        ctx['exercises_done'] = problems_done
 
248
        if problems_total > 0:
 
249
            if problems_done >= problems_total:
 
250
                ctx['worksheets_complete_class'] = "complete"
 
251
            elif problems_done > 0:
 
252
                ctx['worksheets_complete_class'] = "semicomplete"
 
253
            else:
 
254
                ctx['worksheets_complete_class'] = "incomplete"
 
255
            # Calculate the final percentage and mark for the subject
 
256
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
257
             ctx['worksheet_max_mark']) = (
 
258
                ivle.worksheet.utils.calculate_mark(
 
259
                    problems_done, problems_total))
 
260
 
 
261
 
 
262
class SubjectValidator(formencode.FancyValidator):
 
263
    """A FormEncode validator that turns a subject name into a subject.
 
264
 
 
265
    The state must have a 'store' attribute, which is the Storm store
 
266
    to use.
 
267
    """
 
268
    def _to_python(self, value, state):
 
269
        subject = state.store.find(Subject, short_name=value).one()
 
270
        if subject:
 
271
            return subject
 
272
        else:
 
273
            raise formencode.Invalid('Subject does not exist', value, state)
 
274
 
 
275
 
 
276
class SemesterValidator(formencode.FancyValidator):
 
277
    """A FormEncode validator that turns a string into a semester.
 
278
 
 
279
    The string should be of the form 'year/semester', eg. '2009/1'.
 
280
 
 
281
    The state must have a 'store' attribute, which is the Storm store
 
282
    to use.
 
283
    """
 
284
    def _to_python(self, value, state):
 
285
        try:
 
286
            year, semester = value.split('/')
 
287
        except ValueError:
 
288
            year = semester = None
 
289
 
 
290
        semester = state.store.find(
 
291
            Semester, year=year, semester=semester).one()
 
292
        if semester:
 
293
            return semester
 
294
        else:
 
295
            raise formencode.Invalid('Semester does not exist', value, state)
 
296
 
 
297
 
 
298
class OfferingUniquenessValidator(formencode.FancyValidator):
 
299
    """A FormEncode validator that checks that an offering is unique.
 
300
 
 
301
    There cannot be more than one offering in the same year and semester.
 
302
 
 
303
    The offering referenced by state.existing_offering is permitted to
 
304
    hold that year and semester tuple. If any other object holds it, the
 
305
    input is rejected.
 
306
    """
 
307
    def _to_python(self, value, state):
 
308
        if (state.store.find(
 
309
                Offering, subject=value['subject'],
 
310
                semester=value['semester']).one() not in
 
311
                (None, state.existing_offering)):
 
312
            raise formencode.Invalid(
 
313
                'Offering already exists', value, state)
 
314
        return value
 
315
 
 
316
 
 
317
class OfferingSchema(formencode.Schema):
 
318
    description = formencode.validators.UnicodeString(
 
319
        if_missing=None, not_empty=False)
 
320
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
321
 
 
322
 
 
323
class OfferingAdminSchema(OfferingSchema):
 
324
    subject = formencode.All(
 
325
        SubjectValidator(), formencode.validators.UnicodeString())
 
326
    semester = formencode.All(
 
327
        SemesterValidator(), formencode.validators.UnicodeString())
 
328
    chained_validators = [OfferingUniquenessValidator()]
 
329
 
 
330
 
 
331
class OfferingEdit(BaseFormView):
 
332
    """A form to edit an offering's details."""
 
333
    template = 'templates/offering-edit.html'
 
334
    tab = 'subjects'
 
335
    permission = 'edit'
 
336
 
 
337
    @property
 
338
    def validator(self):
 
339
        if self.req.user.admin:
 
340
            return OfferingAdminSchema()
 
341
        else:
 
342
            return OfferingSchema()
 
343
 
 
344
    def populate(self, req, ctx):
 
345
        super(OfferingEdit, self).populate(req, ctx)
 
346
        ctx['subjects'] = req.store.find(Subject)
 
347
        ctx['semesters'] = req.store.find(Semester)
 
348
 
 
349
    def populate_state(self, state):
 
350
        state.existing_offering = self.context
 
351
 
 
352
    def get_default_data(self, req):
 
353
        return {
 
354
            'subject': self.context.subject.short_name,
 
355
            'semester': self.context.semester.year + '/' +
 
356
                        self.context.semester.semester,
 
357
            'url': self.context.url,
 
358
            'description': self.context.description,
 
359
            }
 
360
 
 
361
    def save_object(self, req, data):
 
362
        if req.user.admin:
 
363
            self.context.subject = data['subject']
 
364
            self.context.semester = data['semester']
 
365
        self.context.description = data['description']
 
366
        self.context.url = unicode(data['url']) if data['url'] else None
 
367
        return self.context
 
368
 
 
369
 
 
370
class OfferingNew(BaseFormView):
 
371
    """A form to create an offering."""
 
372
    template = 'templates/offering-new.html'
 
373
    tab = 'subjects'
 
374
 
 
375
    def authorize(self, req):
 
376
        return req.user is not None and req.user.admin
 
377
 
 
378
    @property
 
379
    def validator(self):
 
380
        return OfferingAdminSchema()
 
381
 
 
382
    def populate(self, req, ctx):
 
383
        super(OfferingNew, self).populate(req, ctx)
 
384
        ctx['subjects'] = req.store.find(Subject)
 
385
        ctx['semesters'] = req.store.find(Semester)
 
386
 
 
387
    def populate_state(self, state):
 
388
        state.existing_offering = None
 
389
 
 
390
    def get_default_data(self, req):
 
391
        return {}
 
392
 
 
393
    def save_object(self, req, data):
 
394
        new_offering = Offering()
 
395
        new_offering.subject = data['subject']
 
396
        new_offering.semester = data['semester']
 
397
        new_offering.description = data['description']
 
398
        new_offering.url = unicode(data['url']) if data['url'] else None
 
399
 
 
400
        req.store.add(new_offering)
 
401
        return new_offering
 
402
 
 
403
 
 
404
class UserValidator(formencode.FancyValidator):
 
405
    """A FormEncode validator that turns a username into a user.
 
406
 
 
407
    The state must have a 'store' attribute, which is the Storm store
 
408
    to use."""
 
409
    def _to_python(self, value, state):
 
410
        user = User.get_by_login(state.store, value)
 
411
        if user:
 
412
            return user
 
413
        else:
 
414
            raise formencode.Invalid('User does not exist', value, state)
 
415
 
 
416
 
 
417
class NoEnrolmentValidator(formencode.FancyValidator):
 
418
    """A FormEncode validator that ensures absence of an enrolment.
 
419
 
 
420
    The state must have an 'offering' attribute.
 
421
    """
 
422
    def _to_python(self, value, state):
 
423
        if state.offering.get_enrolment(value):
 
424
            raise formencode.Invalid('User already enrolled', value, state)
 
425
        return value
 
426
 
 
427
 
 
428
class RoleEnrolmentValidator(formencode.FancyValidator):
 
429
    """A FormEncode validator that checks permission to enrol users with a
 
430
    particular role.
 
431
 
 
432
    The state must have an 'offering' attribute.
 
433
    """
 
434
    def _to_python(self, value, state):
 
435
        if (("enrol_" + value) not in
 
436
                state.offering.get_permissions(state.user, state.config)):
 
437
            raise formencode.Invalid('Not allowed to assign users that role',
 
438
                                     value, state)
 
439
        return value
 
440
 
 
441
 
 
442
class EnrolSchema(formencode.Schema):
 
443
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
444
    role = formencode.All(formencode.validators.OneOf(
 
445
                                ["lecturer", "tutor", "student"]),
 
446
                          RoleEnrolmentValidator(),
 
447
                          formencode.validators.UnicodeString())
 
448
 
 
449
 
 
450
class EnrolmentsView(XHTMLView):
 
451
    """A page which displays all users enrolled in an offering."""
 
452
    template = 'templates/enrolments.html'
 
453
    tab = 'subjects'
 
454
    permission = 'edit'
 
455
 
 
456
    def populate(self, req, ctx):
 
457
        ctx['offering'] = self.context
 
458
 
 
459
class EnrolView(XHTMLView):
 
460
    """A form to enrol a user in an offering."""
 
461
    template = 'templates/enrol.html'
 
462
    tab = 'subjects'
 
463
    permission = 'enrol'
 
464
 
 
465
    def filter(self, stream, ctx):
 
466
        return stream | HTMLFormFiller(data=ctx['data'])
 
467
 
 
468
    def populate(self, req, ctx):
 
469
        if req.method == 'POST':
 
470
            data = dict(req.get_fieldstorage())
 
471
            try:
 
472
                validator = EnrolSchema()
 
473
                req.offering = self.context # XXX: Getting into state.
 
474
                data = validator.to_python(data, state=req)
 
475
                self.context.enrol(data['user'], data['role'])
 
476
                req.store.commit()
 
477
                req.throw_redirect(req.uri)
 
478
            except formencode.Invalid, e:
 
479
                errors = e.unpack_errors()
 
480
        else:
 
481
            data = {}
 
482
            errors = {}
 
483
 
 
484
        ctx['data'] = data or {}
 
485
        ctx['offering'] = self.context
 
486
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
 
487
        ctx['errors'] = errors
 
488
 
 
489
class OfferingProjectsView(XHTMLView):
 
490
    """View the projects for an offering."""
 
491
    template = 'templates/offering_projects.html'
 
492
    permission = 'edit'
 
493
    tab = 'subjects'
 
494
 
 
495
    def populate(self, req, ctx):
 
496
        self.plugin_styles[Plugin] = ["project.css"]
 
497
        self.plugin_scripts[Plugin] = ["project.js"]
 
498
        ctx['req'] = req
 
499
        ctx['offering'] = self.context
 
500
        ctx['projectsets'] = []
 
501
        ctx['OfferingRESTView'] = OfferingRESTView
 
502
 
 
503
        #Open the projectset Fragment, and render it for inclusion
 
504
        #into the ProjectSets page
 
505
        #XXX: This could be a lot cleaner
 
506
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
507
 
 
508
        set_fragment = os.path.join(os.path.dirname(__file__),
 
509
                "templates/projectset_fragment.html")
 
510
        project_fragment = os.path.join(os.path.dirname(__file__),
 
511
                "templates/project_fragment.html")
 
512
 
 
513
        for projectset in self.context.project_sets:
 
514
            settmpl = loader.load(set_fragment)
 
515
            setCtx = Context()
 
516
            setCtx['req'] = req
 
517
            setCtx['projectset'] = projectset
 
518
            setCtx['projects'] = []
 
519
            setCtx['GroupsView'] = GroupsView
 
520
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
521
 
 
522
            for project in projectset.projects:
 
523
                projecttmpl = loader.load(project_fragment)
 
524
                projectCtx = Context()
 
525
                projectCtx['req'] = req
 
526
                projectCtx['project'] = project
 
527
 
 
528
                setCtx['projects'].append(
 
529
                        projecttmpl.generate(projectCtx))
 
530
 
 
531
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
532
 
 
533
 
 
534
class ProjectView(XHTMLView):
 
535
    """View the submissions for a ProjectSet"""
 
536
    template = "templates/project.html"
 
537
    permission = "view_project_submissions"
 
538
    tab = 'subjects'
 
539
 
 
540
    def build_subversion_url(self, svnroot, submission):
 
541
        princ = submission.assessed.principal
 
542
 
 
543
        if isinstance(princ, User):
 
544
            path = 'users/%s' % princ.login
 
545
        else:
 
546
            path = 'groups/%s_%s_%s_%s' % (
 
547
                    princ.project_set.offering.subject.short_name,
 
548
                    princ.project_set.offering.semester.year,
 
549
                    princ.project_set.offering.semester.semester,
 
550
                    princ.name
 
551
                    )
 
552
        return urlparse.urljoin(
 
553
                    svnroot,
 
554
                    os.path.join(path, submission.path[1:] if
 
555
                                       submission.path.startswith(os.sep) else
 
556
                                       submission.path))
 
557
 
 
558
    def populate(self, req, ctx):
 
559
        self.plugin_styles[Plugin] = ["project.css"]
 
560
 
 
561
        ctx['req'] = req
 
562
        ctx['GroupsView'] = GroupsView
 
563
        ctx['EnrolView'] = EnrolView
 
564
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
565
        ctx['build_subversion_url'] = self.build_subversion_url
 
566
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
567
        ctx['project'] = self.context
 
568
        ctx['user'] = req.user
53
569
 
54
570
class Plugin(ViewPlugin, MediaPlugin):
55
 
    urls = [
56
 
        ('subjects/', SubjectsView),
57
 
    ]
 
571
    forward_routes = (root_to_subject, subject_to_offering,
 
572
                      offering_to_project, offering_to_projectset)
 
573
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
574
 
 
575
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
576
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
577
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
 
578
             (ApplicationRoot, ('subjects', '+new-semester'), SemesterNew),
 
579
             (Subject, '+edit', SubjectEdit),
 
580
             (Offering, '+index', OfferingView),
 
581
             (Offering, '+edit', OfferingEdit),
 
582
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
583
             (Offering, ('+enrolments', '+new'), EnrolView),
 
584
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
585
             (Project, '+index', ProjectView),
 
586
 
 
587
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
588
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
589
             ]
 
590
 
 
591
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
592
                   Offering: OfferingBreadcrumb,
 
593
                   User: UserBreadcrumb,
 
594
                   Project: ProjectBreadcrumb,
 
595
                   }
58
596
 
59
597
    tabs = [
60
598
        ('subjects', 'Subjects',