~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-01 08:06:30 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:376
auth/authenticate: Replaced dummy code (which always auths) with a call to the
    DB. Now correctly authenticates.
    IMPORTANT: Changed interface. Instead of returning True/False, now returns
    None or a dictionary of the login details.
    I have chased this up with all the callers (just 1).
dispatch/login.py: Reworked call to authenticate. Now handles the change
    above, and retrieves the login details. Writes all of the user's login
    fields to the browser session (see debuginfo to see the fields stored
    there).

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