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

« back to all changes in this revision

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

  • Committer: stevenbird
  • Date: 2008-02-19 21:17:21 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:512
Renaming of problems to exercises (initial commit).
Fix up module naming (exercises sometimes called tutorials).

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