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

« back to all changes in this revision

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

  • Committer: Matt Giuca
  • Date: 2010-02-12 04:00:39 UTC
  • Revision ID: matt.giuca@gmail.com-20100212040039-vw9yf8p4s98g6nu9
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.

Show diffs side-by-side

added added

removed removed

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