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

« back to all changes in this revision

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

  • Committer: mattgiuca
  • Date: 2008-01-25 06:20:32 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:313
test/test_framework: Updated examples, a bit of better descriptions, sample
    partial solutions, etc.

Added all sample subject material.
This has been copied from test/test_framework and modified slightly.
There is now a subject "sample" with 2 worksheets. The 1st worksheet has 3
exercises. These work in IVLE by default.

setup.py: Added code to install subjects into the designated directory. This
means that installing IVLE will result in the sample subjects being
immediately available.

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