~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 08:49:58 UTC
  • Revision ID: grantw@unimelb.edu.au-20100215084958-8x5dzd9k4pbcddlz
Split subject/semester management out onto a separate page, and link to SemesterEdit.

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