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

621 by mattgiuca
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
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
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
25
import os
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
26
import os.path
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
27
import urllib
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
28
import urlparse
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
29
import cgi
30
1294.2.52 by William Grant
Port subject-related views to object traversal.
31
from storm.locals import Desc, Store
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
32
import genshi
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
33
from genshi.filters import HTMLFormFiller
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
34
from genshi.template import Context, TemplateLoader
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
35
import formencode
1125 by William Grant
Rework ivle.webapp.admin.subjects#SubjectsView to split offerings nicely by
36
1099.1.34 by William Grant
Split up ivle.webapp.base.views into ivle.webapp.base.{rest,xhtml}, as it was
37
from ivle.webapp.base.xhtml import XHTMLView
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
1294.2.52 by William Grant
Port subject-related views to object traversal.
39
from ivle.webapp import ApplicationRoot
1165.3.9 by Nick Chadwick
merge from trunk
40
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
1165.3.8 by Nick Chadwick
Added a total submissions and total assesseds to the project view
42
                          ProjectSet, Project, ProjectSubmission
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
43
from ivle import util
1165.3.14 by William Grant
Improve ProjectView's template substantially.
44
import ivle.date
621 by mattgiuca
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
45
1375.1.2 by William Grant
Remove ProjectRESTView, which did nothing and was unused.
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
47
from ivle.webapp.admin.offeringservice import OfferingRESTView
1294.3.2 by William Grant
Router->Publisher
48
from ivle.webapp.admin.publishing import (root_to_subject,
1294.2.70 by William Grant
Split out ivle.webapp.admin's routes into annotated functions in ivle.webapp.traversal.
49
            subject_to_offering, offering_to_projectset, offering_to_project,
50
            subject_url, offering_url, projectset_url, project_url)
1294.2.96 by William Grant
Add a UserBreadcrumb.
51
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
1294.2.98 by William Grant
Add a ProjectBreadcrumb.
52
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
53
from ivle.webapp.groups import GroupsView
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
54
from ivle.webapp.tutorial import Plugin as TutorialPlugin
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
55
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
56
class SubjectsView(XHTMLView):
57
    '''The view of the list of subjects.'''
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
58
    template = 'templates/subjects.html'
1116 by William Grant
Move the old tutorial views into the 'subjects' tab, so they get the right
59
    tab = 'subjects'
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
60
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
61
    def authorize(self, req):
1138 by William Grant
SubjectsView now tells users if they have no enrolments.
62
        return req.user is not None
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
63
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
64
    def populate(self, req, ctx):
1139 by William Grant
Show group administration links on SubjectsView where privileges allow it.
65
        ctx['user'] = req.user
1125 by William Grant
Rework ivle.webapp.admin.subjects#SubjectsView to split offerings nicely by
66
        ctx['semesters'] = []
67
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
68
                                                     Desc(Semester.semester)):
1372 by Matt Giuca
admin/subject: Admin users now see all offerings in the system, not just the ones they are enrolled in.
69
            if req.user.admin:
70
                # For admins, show all subjects in the system
71
                offerings = list(semester.offerings.find())
72
            else:
73
                offerings = [enrolment.offering for enrolment in
1371 by Matt Giuca
admin/subject: Now sends a list of offerings the user is enrolled in to the Genshi template, rather than a list of enrolment objects (of which only the offering is observed). This allows us to send non-enrolment offerings to the template.
74
                                    semester.enrolments.find(user=req.user)]
75
            if len(offerings):
76
                ctx['semesters'].append((semester, offerings))
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
77
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
78
1442.1.15 by William Grant
Show the group name in the case of a group project.
79
def format_submission_principal(user, principal):
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
80
    """Render a list of users to fit in the offering project listing.
81
82
    Given a user and a list of submitters, returns 'solo' if the
83
    only submitter is the user, or a string of the form
84
    'with A, B and C' if there are any other submitters.
85
86
    If submitters is None, we assume that the list of members could
87
    not be determined, so we just return 'group'.
88
    """
1442.1.15 by William Grant
Show the group name in the case of a group project.
89
    if principal is None:
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
90
        return 'group'
91
1442.1.15 by William Grant
Show the group name in the case of a group project.
92
    if principal is user:
93
        return 'solo'
94
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
95
    display_names = sorted(
1442.1.15 by William Grant
Show the group name in the case of a group project.
96
        member.display_name for member in principal.members
97
        if member is not user)
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
98
99
    if len(display_names) == 0:
1442.1.15 by William Grant
Show the group name in the case of a group project.
100
        return 'solo (%s)' % principal.name
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
101
    elif len(display_names) == 1:
1442.1.15 by William Grant
Show the group name in the case of a group project.
102
        return 'with %s (%s)' % (display_names[0], principal.name)
1442.1.16 by William Grant
Don't ever display more than five group member names in the project listing.
103
    elif len(display_names) > 5:
104
        return 'with %d others (%s)' % (len(display_names), principal.name)
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
105
    else:
1442.1.15 by William Grant
Show the group name in the case of a group project.
106
        return 'with %s and %s (%s)' % (', '.join(display_names[:-1]),
107
                                        display_names[-1], principal.name)
1442.1.12 by William Grant
Display either 'solo' or the list of other group members on the offering project listing.
108
109
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
110
class OfferingView(XHTMLView):
111
    """The home page of an offering."""
112
    template = 'templates/offering.html'
113
    tab = 'subjects'
114
    permission = 'view'
115
116
    def populate(self, req, ctx):
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
117
        # Need the worksheet result styles.
118
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
119
        ctx['context'] = self.context
120
        ctx['req'] = req
1442.1.5 by William Grant
Improve worksheet management experience from the offering index.
121
        ctx['permissions'] = self.context.get_permissions(req.user)
1442.1.15 by William Grant
Show the group name in the case of a group project.
122
        ctx['format_submission_principal'] = format_submission_principal
1442.1.10 by William Grant
Add a nice padded list of projects.
123
        ctx['format_datetime'] = ivle.date.make_date_nice
124
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
1451.1.7 by William Grant
Add a 'Change details' link on the offering index, pointing to +edit.
125
        ctx['OfferingEdit'] = OfferingEdit
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
126
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
127
        # As we go, calculate the total score for this subject
128
        # (Assessable worksheets only, mandatory problems only)
129
130
        ctx['worksheets'], problems_total, problems_done = (
131
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
132
                req.store, req.user, self.context))
133
134
        ctx['exercises_total'] = problems_total
135
        ctx['exercises_done'] = problems_done
136
        if problems_total > 0:
137
            if problems_done >= problems_total:
138
                ctx['worksheets_complete_class'] = "complete"
139
            elif problems_done > 0:
140
                ctx['worksheets_complete_class'] = "semicomplete"
141
            else:
142
                ctx['worksheets_complete_class'] = "incomplete"
143
            # Calculate the final percentage and mark for the subject
144
            (ctx['exercises_pct'], ctx['worksheet_mark'],
145
             ctx['worksheet_max_mark']) = (
146
                ivle.worksheet.utils.calculate_mark(
147
                    problems_done, problems_total))
148
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
149
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
150
class OfferingSchema(formencode.Schema):
1451.1.8 by William Grant
Allow unsetting of the URL or description.
151
    description = formencode.validators.UnicodeString(
152
        if_missing=None, not_empty=False)
153
    url = formencode.validators.URL(if_missing=None, not_empty=False)
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
154
155
156
class OfferingEdit(XHTMLView):
157
    """A form to edit an offering's details."""
158
    template = 'templates/offering-edit.html'
159
    permission = 'edit'
160
161
    def filter(self, stream, ctx):
162
        return stream | HTMLFormFiller(data=ctx['data'])
163
164
    def populate(self, req, ctx):
165
        if req.method == 'POST':
166
            data = dict(req.get_fieldstorage())
167
            try:
168
                validator = OfferingSchema()
169
                data = validator.to_python(data, state=req)
170
1451.1.8 by William Grant
Allow unsetting of the URL or description.
171
                self.context.url = unicode(data['url']) if data['url'] else None
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
172
                self.context.description = data['description']
173
                req.store.commit()
174
                req.throw_redirect(req.publisher.generate(self.context))
175
            except formencode.Invalid, e:
176
                errors = e.unpack_errors()
177
        else:
178
            data = {
179
                'url': self.context.url,
180
                'description': self.context.description,
181
            }
182
            errors = {}
183
184
        ctx['data'] = data or {}
185
        ctx['context'] = self.context
186
        ctx['errors'] = errors
187
188
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
189
class UserValidator(formencode.FancyValidator):
190
    """A FormEncode validator that turns a username into a user.
191
192
    The state must have a 'store' attribute, which is the Storm store
193
    to use."""
194
    def _to_python(self, value, state):
195
        user = User.get_by_login(state.store, value)
196
        if user:
197
            return user
198
        else:
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
199
            raise formencode.Invalid('User does not exist', value, state)
200
201
202
class NoEnrolmentValidator(formencode.FancyValidator):
203
    """A FormEncode validator that ensures absence of an enrolment.
204
205
    The state must have an 'offering' attribute.
206
    """
207
    def _to_python(self, value, state):
208
        if state.offering.get_enrolment(value):
209
            raise formencode.Invalid('User already enrolled', value, state)
210
        return value
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
211
212
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
213
class RoleEnrolmentValidator(formencode.FancyValidator):
214
    """A FormEncode validator that checks permission to enrol users with a
215
    particular role.
216
217
    The state must have an 'offering' attribute.
218
    """
219
    def _to_python(self, value, state):
220
        if ("enrol_" + value) not in state.offering.get_permissions(state.user):
221
            raise formencode.Invalid('Not allowed to assign users that role',
222
                                     value, state)
223
        return value
224
225
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
226
class EnrolSchema(formencode.Schema):
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
227
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
228
    role = formencode.All(formencode.validators.OneOf(
229
                                ["lecturer", "tutor", "student"]),
230
                          RoleEnrolmentValidator(),
231
                          formencode.validators.UnicodeString())
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
232
233
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
234
class EnrolmentsView(XHTMLView):
235
    """A page which displays all users enrolled in an offering."""
236
    template = 'templates/enrolments.html'
237
    permission = 'edit'
238
239
    def populate(self, req, ctx):
240
        ctx['offering'] = self.context
241
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
242
class EnrolView(XHTMLView):
243
    """A form to enrol a user in an offering."""
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
244
    template = 'templates/enrol.html'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
245
    tab = 'subjects'
1376 by Matt Giuca
database: More granular permissions on offerings: Added 'enrol' permission.
246
    permission = 'enrol'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
247
248
    def filter(self, stream, ctx):
249
        return stream | HTMLFormFiller(data=ctx['data'])
250
251
    def populate(self, req, ctx):
252
        if req.method == 'POST':
253
            data = dict(req.get_fieldstorage())
254
            try:
255
                validator = EnrolSchema()
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
256
                req.offering = self.context # XXX: Getting into state.
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
257
                data = validator.to_python(data, state=req)
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
258
                self.context.enrol(data['user'], data['role'])
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
259
                req.store.commit()
260
                req.throw_redirect(req.uri)
261
            except formencode.Invalid, e:
262
                errors = e.unpack_errors()
263
        else:
264
            data = {}
265
            errors = {}
266
267
        ctx['data'] = data or {}
268
        ctx['offering'] = self.context
1379 by Matt Giuca
Enrolments page: Don't show choices in the list if they are unauthorized. (Note that this is a UI tweak -- roles are already authz-checked.)
269
        ctx['roles_auth'] = self.context.get_permissions(req.user)
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
270
        ctx['errors'] = errors
271
1165.3.19 by William Grant
Rename SubjectProjectSetView to OfferingProjectsView.
272
class OfferingProjectsView(XHTMLView):
273
    """View the projects for an offering."""
274
    template = 'templates/offering_projects.html'
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
275
    permission = 'edit'
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
276
    tab = 'subjects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
277
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
278
    def populate(self, req, ctx):
279
        self.plugin_styles[Plugin] = ["project.css"]
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
280
        self.plugin_scripts[Plugin] = ["project.js"]
1361 by William Grant
Remove the last +projectsets hardcoding.
281
        ctx['req'] = req
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
282
        ctx['offering'] = self.context
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
283
        ctx['projectsets'] = []
1361 by William Grant
Remove the last +projectsets hardcoding.
284
        ctx['OfferingRESTView'] = OfferingRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
285
286
        #Open the projectset Fragment, and render it for inclusion
287
        #into the ProjectSets page
288
        #XXX: This could be a lot cleaner
289
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
290
291
        set_fragment = os.path.join(os.path.dirname(__file__),
292
                "templates/projectset_fragment.html")
293
        project_fragment = os.path.join(os.path.dirname(__file__),
294
                "templates/project_fragment.html")
295
296
        for projectset in self.context.project_sets:
297
            settmpl = loader.load(set_fragment)
298
            setCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
299
            setCtx['req'] = req
1165.3.30 by William Grant
Clean out the projectset fragment context.
300
            setCtx['projectset'] = projectset
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
301
            setCtx['projects'] = []
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
302
            setCtx['GroupsView'] = GroupsView
303
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
304
305
            for project in projectset.projects:
306
                projecttmpl = loader.load(project_fragment)
307
                projectCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
308
                projectCtx['req'] = req
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
309
                projectCtx['project'] = project
310
311
                setCtx['projects'].append(
312
                        projecttmpl.generate(projectCtx))
313
314
            ctx['projectsets'].append(settmpl.generate(setCtx))
315
316
317
class ProjectView(XHTMLView):
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
318
    """View the submissions for a ProjectSet"""
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
319
    template = "templates/project.html"
320
    permission = "edit"
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
321
    tab = 'subjects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
322
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
323
    def build_subversion_url(self, svnroot, submission):
324
        princ = submission.assessed.principal
325
326
        if isinstance(princ, User):
327
            path = 'users/%s' % princ.login
328
        else:
329
            path = 'groups/%s_%s_%s_%s' % (
330
                    princ.project_set.offering.subject.short_name,
331
                    princ.project_set.offering.semester.year,
332
                    princ.project_set.offering.semester.semester,
333
                    princ.name
334
                    )
335
        return urlparse.urljoin(
336
                    svnroot,
337
                    os.path.join(path, submission.path[1:] if
338
                                       submission.path.startswith(os.sep) else
339
                                       submission.path))
340
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
341
    def populate(self, req, ctx):
1165.3.66 by William Grant
Prettify the submissions table.
342
        self.plugin_styles[Plugin] = ["project.css"]
343
1375.1.4 by William Grant
Indicate when there is nobody assigned to a project, and link to the page to fix that.
344
        ctx['req'] = req
345
        ctx['GroupsView'] = GroupsView
346
        ctx['EnrolView'] = EnrolView
1165.3.14 by William Grant
Improve ProjectView's template substantially.
347
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
348
        ctx['build_subversion_url'] = self.build_subversion_url
349
        ctx['svn_addr'] = req.config['urls']['svn_addr']
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
350
        ctx['project'] = self.context
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
351
        ctx['user'] = req.user
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
352
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
353
class Plugin(ViewPlugin, MediaPlugin):
1294.2.70 by William Grant
Split out ivle.webapp.admin's routes into annotated functions in ivle.webapp.traversal.
354
    forward_routes = (root_to_subject, subject_to_offering,
355
                      offering_to_project, offering_to_projectset)
356
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
1294.2.52 by William Grant
Port subject-related views to object traversal.
357
358
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
359
             (Offering, '+index', OfferingView),
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
360
             (Offering, '+edit', OfferingEdit),
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
361
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
1294.2.52 by William Grant
Port subject-related views to object traversal.
362
             (Offering, ('+enrolments', '+new'), EnrolView),
363
             (Offering, ('+projects', '+index'), OfferingProjectsView),
364
             (Project, '+index', ProjectView),
365
366
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
367
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
368
             ]
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
369
1294.2.94 by William Grant
Add a SubjectBreadcrumb.
370
    breadcrumbs = {Subject: SubjectBreadcrumb,
371
                   Offering: OfferingBreadcrumb,
1294.2.96 by William Grant
Add a UserBreadcrumb.
372
                   User: UserBreadcrumb,
1294.2.98 by William Grant
Add a ProjectBreadcrumb.
373
                   Project: ProjectBreadcrumb,
1294.2.89 by William Grant
Add an Offering breadcrumb.
374
                   }
375
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
376
    tabs = [
1118 by matt.giuca
Rewrote tooltips for the four tabs visible by default.
377
        ('subjects', 'Subjects',
378
         'View subject content and complete worksheets',
379
         'subjects.png', 'subjects', 5)
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
380
    ]
381
382
    media = 'subject-media'