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

« back to all changes in this revision

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

  • Committer: mattgiuca
  • Date: 2007-12-20 05:25:03 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:103
Fix to Makefile.

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,
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
59
 
 
60
 
class SubjectsView(XHTMLView):
61
 
    '''The view of the list of subjects.'''
62
 
    template = 'templates/subjects.html'
63
 
    tab = 'subjects'
64
 
 
65
 
    def authorize(self, req):
66
 
        return req.user is not None
67
 
 
68
 
    def populate(self, req, ctx):
69
 
        ctx['req'] = req
70
 
        ctx['user'] = req.user
71
 
        ctx['semesters'] = []
72
 
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
73
 
        ctx['SubjectEdit'] = SubjectEdit
74
 
 
75
 
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
76
 
                                                     Desc(Semester.semester)):
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
569
 
 
570
 
class Plugin(ViewPlugin, MediaPlugin):
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
 
                   }
596
 
 
597
 
    tabs = [
598
 
        ('subjects', 'Subjects',
599
 
         'View subject content and complete worksheets',
600
 
         'subjects.png', 'subjects', 5)
601
 
    ]
602
 
 
603
 
    media = 'subject-media'