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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2010-02-17 08:37:22 UTC
  • Revision ID: grantw@unimelb.edu.au-20100217083722-ji4v6fbztngy0p3s
Add UI to edit/delete enrolments.

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
 
 
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
29
40
from ivle.webapp.base.xhtml import XHTMLView
30
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
 
from ivle.webapp.errors import NotFound
32
 
from ivle.database import Subject
 
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
33
46
from ivle import util
 
47
import ivle.date
34
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
35
61
 
36
62
class SubjectsView(XHTMLView):
37
63
    '''The view of the list of subjects.'''
38
 
    template = 'subjects.html'
39
 
    appname = 'subjects' # XXX
 
64
    template = 'templates/subjects.html'
 
65
    tab = 'subjects'
40
66
 
41
67
    def authorize(self, req):
42
68
        return req.user is not None
43
69
 
44
70
    def populate(self, req, ctx):
45
 
        enrolled_subjects = req.user.subjects
46
 
        unenrolled_subjects = [subject for subject in
47
 
                               req.store.find(Subject)
48
 
                               if subject not in enrolled_subjects]
49
 
 
50
 
        ctx['enrolled_subjects'] = []
51
 
        ctx['other_subjects'] = []
52
 
 
53
 
        for subject in enrolled_subjects:
54
 
            new_subj = {}
55
 
            new_subj['name'] = subject.name
56
 
            new_subj['url'] = subject.url
57
 
            ctx['enrolled_subjects'].append(new_subj)
58
 
 
59
 
        if len(unenrolled_subjects) > 0:
60
 
            for subject in unenrolled_subjects:
61
 
                new_subj = {}
62
 
                new_subj['name'] = subject.name
63
 
                new_subj['url'] = subject.url
64
 
                ctx['other_subjects'].append(new_subj)
65
 
 
 
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
66
724
 
67
725
class Plugin(ViewPlugin, MediaPlugin):
68
 
    urls = [
69
 
        ('subjects/', SubjectsView),
70
 
    ]
 
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
                   }
71
759
 
72
760
    tabs = [
73
 
        ('subjects', 'Subjects', 'Announcements and information about the '
74
 
         'subjects you are enrolled in.', 'subjects.png', 'subjects', 5)
 
761
        ('subjects', 'Subjects',
 
762
         'View subject content and complete worksheets',
 
763
         'subjects.png', 'subjects', 5)
75
764
    ]
76
765
 
77
766
    media = 'subject-media'