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

« back to all changes in this revision

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

  • Committer: drtomc
  • Date: 2007-12-11 03:26:29 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:25
A bit more work on the userdb stuff.

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
35
 
import formencode
36
 
import formencode.validators
37
 
 
38
 
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
39
 
                                    DateTimeValidator)
40
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
41
 
from ivle.webapp.base.xhtml import XHTMLView
42
 
from ivle.webapp.errors import BadRequest
43
 
from ivle.webapp import ApplicationRoot
44
 
 
45
 
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
46
 
                          ProjectSet, Project, ProjectSubmission
47
 
from ivle import util
48
 
import ivle.date
49
 
 
50
 
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
51
 
            subject_to_offering, offering_to_projectset, offering_to_project,
52
 
            offering_to_enrolment, subject_url, semester_url, offering_url,
53
 
            projectset_url, project_url, enrolment_url)
54
 
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
55
 
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
56
 
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
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
 
    breadcrumb_text = "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['SubjectView'] = SubjectView
100
 
        ctx['SubjectEdit'] = SubjectEdit
101
 
        ctx['SemesterEdit'] = SemesterEdit
102
 
 
103
 
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
104
 
        ctx['semesters'] = req.store.find(Semester).order_by(
105
 
            Semester.year, Semester.semester)
106
 
 
107
 
 
108
 
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
109
 
    """A FormEncode validator that checks that a subject name is unused.
110
 
 
111
 
    The subject referenced by state.existing_subject is permitted
112
 
    to hold that name. If any other object holds it, the input is rejected.
113
 
    """
114
 
    def __init__(self, matching=None):
115
 
        self.matching = matching
116
 
 
117
 
    def _to_python(self, value, state):
118
 
        if (state.store.find(
119
 
                Subject, short_name=value).one() not in
120
 
                (None, state.existing_subject)):
121
 
            raise formencode.Invalid(
122
 
                'Short name already taken', value, state)
123
 
        return value
124
 
 
125
 
 
126
 
class SubjectSchema(formencode.Schema):
127
 
    short_name = formencode.All(
128
 
        SubjectShortNameUniquenessValidator(),
129
 
        URLNameValidator(not_empty=True))
130
 
    name = formencode.validators.UnicodeString(not_empty=True)
131
 
    code = formencode.validators.UnicodeString(not_empty=True)
132
 
 
133
 
 
134
 
class SubjectFormView(BaseFormView):
135
 
    """An abstract form to add or edit a subject."""
136
 
    tab = 'subjects'
137
 
 
138
 
    def authorize(self, req):
139
 
        return req.user is not None and req.user.admin
140
 
 
141
 
    def populate_state(self, state):
142
 
        state.existing_subject = None
143
 
 
144
 
    @property
145
 
    def validator(self):
146
 
        return SubjectSchema()
147
 
 
148
 
 
149
 
class SubjectNew(SubjectFormView):
150
 
    """A form to create a subject."""
151
 
    template = 'templates/subject-new.html'
152
 
 
153
 
    def get_default_data(self, req):
154
 
        return {}
155
 
 
156
 
    def save_object(self, req, data):
157
 
        new_subject = Subject()
158
 
        new_subject.short_name = data['short_name']
159
 
        new_subject.name = data['name']
160
 
        new_subject.code = data['code']
161
 
 
162
 
        req.store.add(new_subject)
163
 
        return new_subject
164
 
 
165
 
 
166
 
class SubjectEdit(SubjectFormView):
167
 
    """A form to edit a subject."""
168
 
    template = 'templates/subject-edit.html'
169
 
 
170
 
    def populate_state(self, state):
171
 
        state.existing_subject = self.context
172
 
 
173
 
    def get_default_data(self, req):
174
 
        return {
175
 
            'short_name': self.context.short_name,
176
 
            'name': self.context.name,
177
 
            'code': self.context.code,
178
 
            }
179
 
 
180
 
    def save_object(self, req, data):
181
 
        self.context.short_name = data['short_name']
182
 
        self.context.name = data['name']
183
 
        self.context.code = data['code']
184
 
 
185
 
        return self.context
186
 
 
187
 
 
188
 
class SemesterUniquenessValidator(formencode.FancyValidator):
189
 
    """A FormEncode validator that checks that a semester is unique.
190
 
 
191
 
    There cannot be more than one semester for the same year and semester.
192
 
    """
193
 
    def _to_python(self, value, state):
194
 
        if (state.store.find(
195
 
                Semester, year=value['year'], semester=value['semester']
196
 
                ).one() not in (None, state.existing_semester)):
197
 
            raise formencode.Invalid(
198
 
                'Semester already exists', value, state)
199
 
        return value
200
 
 
201
 
 
202
 
class SemesterSchema(formencode.Schema):
203
 
    year = URLNameValidator()
204
 
    semester = URLNameValidator()
205
 
    state = formencode.All(
206
 
        formencode.validators.OneOf(["past", "current", "future"]),
207
 
        formencode.validators.UnicodeString())
208
 
    chained_validators = [SemesterUniquenessValidator()]
209
 
 
210
 
 
211
 
class SemesterFormView(BaseFormView):
212
 
    tab = 'subjects'
213
 
 
214
 
    def authorize(self, req):
215
 
        return req.user is not None and req.user.admin
216
 
 
217
 
    @property
218
 
    def validator(self):
219
 
        return SemesterSchema()
220
 
 
221
 
    def get_return_url(self, obj):
222
 
        return '/subjects/+manage'
223
 
 
224
 
 
225
 
class SemesterNew(SemesterFormView):
226
 
    """A form to create a semester."""
227
 
    template = 'templates/semester-new.html'
228
 
    tab = 'subjects'
229
 
 
230
 
    def populate_state(self, state):
231
 
        state.existing_semester = None
232
 
 
233
 
    def get_default_data(self, req):
234
 
        return {}
235
 
 
236
 
    def save_object(self, req, data):
237
 
        new_semester = Semester()
238
 
        new_semester.year = data['year']
239
 
        new_semester.semester = data['semester']
240
 
        new_semester.state = data['state']
241
 
 
242
 
        req.store.add(new_semester)
243
 
        return new_semester
244
 
 
245
 
 
246
 
class SemesterEdit(SemesterFormView):
247
 
    """A form to edit a semester."""
248
 
    template = 'templates/semester-edit.html'
249
 
 
250
 
    def populate_state(self, state):
251
 
        state.existing_semester = self.context
252
 
 
253
 
    def get_default_data(self, req):
254
 
        return {
255
 
            'year': self.context.year,
256
 
            'semester': self.context.semester,
257
 
            'state': self.context.state,
258
 
            }
259
 
 
260
 
    def save_object(self, req, data):
261
 
        self.context.year = data['year']
262
 
        self.context.semester = data['semester']
263
 
        self.context.state = data['state']
264
 
 
265
 
        return self.context
266
 
 
267
 
class SubjectView(XHTMLView):
268
 
    '''The view of the list of offerings in a given subject.'''
269
 
    template = 'templates/subject.html'
270
 
    tab = 'subjects'
271
 
 
272
 
    def authorize(self, req):
273
 
        return req.user is not None
274
 
 
275
 
    def populate(self, req, ctx):
276
 
        ctx['context'] = self.context
277
 
        ctx['req'] = req
278
 
        ctx['user'] = req.user
279
 
        ctx['offerings'] = list(self.context.offerings)
280
 
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
281
 
        ctx['SubjectEdit'] = SubjectEdit
282
 
        ctx['SubjectOfferingNew'] = SubjectOfferingNew
283
 
 
284
 
 
285
 
class OfferingView(XHTMLView):
286
 
    """The home page of an offering."""
287
 
    template = 'templates/offering.html'
288
 
    tab = 'subjects'
289
 
    permission = 'view'
290
 
 
291
 
    def populate(self, req, ctx):
292
 
        # Need the worksheet result styles.
293
 
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
294
 
        ctx['context'] = self.context
295
 
        ctx['req'] = req
296
 
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
297
 
        ctx['format_submission_principal'] = util.format_submission_principal
298
 
        ctx['format_datetime'] = ivle.date.make_date_nice
299
 
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
300
 
        ctx['OfferingEdit'] = OfferingEdit
301
 
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
302
 
        ctx['GroupsView'] = GroupsView
303
 
        ctx['EnrolmentsView'] = EnrolmentsView
304
 
 
305
 
        # As we go, calculate the total score for this subject
306
 
        # (Assessable worksheets only, mandatory problems only)
307
 
 
308
 
        ctx['worksheets'], problems_total, problems_done = (
309
 
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
310
 
                req.config, req.store, req.user, self.context))
311
 
 
312
 
        ctx['exercises_total'] = problems_total
313
 
        ctx['exercises_done'] = problems_done
314
 
        if problems_total > 0:
315
 
            if problems_done >= problems_total:
316
 
                ctx['worksheets_complete_class'] = "complete"
317
 
            elif problems_done > 0:
318
 
                ctx['worksheets_complete_class'] = "semicomplete"
319
 
            else:
320
 
                ctx['worksheets_complete_class'] = "incomplete"
321
 
            # Calculate the final percentage and mark for the subject
322
 
            (ctx['exercises_pct'], ctx['worksheet_mark'],
323
 
             ctx['worksheet_max_mark']) = (
324
 
                ivle.worksheet.utils.calculate_mark(
325
 
                    problems_done, problems_total))
326
 
 
327
 
 
328
 
class SubjectValidator(formencode.FancyValidator):
329
 
    """A FormEncode validator that turns a subject name into a subject.
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
 
        subject = state.store.find(Subject, short_name=value).one()
336
 
        if subject:
337
 
            return subject
338
 
        else:
339
 
            raise formencode.Invalid('Subject does not exist', value, state)
340
 
 
341
 
 
342
 
class SemesterValidator(formencode.FancyValidator):
343
 
    """A FormEncode validator that turns a string into a semester.
344
 
 
345
 
    The string should be of the form 'year/semester', eg. '2009/1'.
346
 
 
347
 
    The state must have a 'store' attribute, which is the Storm store
348
 
    to use.
349
 
    """
350
 
    def _to_python(self, value, state):
351
 
        try:
352
 
            year, semester = value.split('/')
353
 
        except ValueError:
354
 
            year = semester = None
355
 
 
356
 
        semester = state.store.find(
357
 
            Semester, year=year, semester=semester).one()
358
 
        if semester:
359
 
            return semester
360
 
        else:
361
 
            raise formencode.Invalid('Semester does not exist', value, state)
362
 
 
363
 
 
364
 
class OfferingUniquenessValidator(formencode.FancyValidator):
365
 
    """A FormEncode validator that checks that an offering is unique.
366
 
 
367
 
    There cannot be more than one offering in the same year and semester.
368
 
 
369
 
    The offering referenced by state.existing_offering is permitted to
370
 
    hold that year and semester tuple. If any other object holds it, the
371
 
    input is rejected.
372
 
    """
373
 
    def _to_python(self, value, state):
374
 
        if (state.store.find(
375
 
                Offering, subject=value['subject'],
376
 
                semester=value['semester']).one() not in
377
 
                (None, state.existing_offering)):
378
 
            raise formencode.Invalid(
379
 
                'Offering already exists', value, state)
380
 
        return value
381
 
 
382
 
 
383
 
class OfferingSchema(formencode.Schema):
384
 
    description = formencode.validators.UnicodeString(
385
 
        if_missing=None, not_empty=False)
386
 
    url = formencode.validators.URL(if_missing=None, not_empty=False)
387
 
    show_worksheet_marks = formencode.validators.StringBoolean(
388
 
        if_missing=False)
389
 
 
390
 
 
391
 
class OfferingAdminSchema(OfferingSchema):
392
 
    subject = formencode.All(
393
 
        SubjectValidator(), formencode.validators.UnicodeString())
394
 
    semester = formencode.All(
395
 
        SemesterValidator(), formencode.validators.UnicodeString())
396
 
    chained_validators = [OfferingUniquenessValidator()]
397
 
 
398
 
 
399
 
class OfferingEdit(BaseFormView):
400
 
    """A form to edit an offering's details."""
401
 
    template = 'templates/offering-edit.html'
402
 
    tab = 'subjects'
403
 
    permission = 'edit'
404
 
 
405
 
    @property
406
 
    def validator(self):
407
 
        if self.req.user.admin:
408
 
            return OfferingAdminSchema()
409
 
        else:
410
 
            return OfferingSchema()
411
 
 
412
 
    def populate(self, req, ctx):
413
 
        super(OfferingEdit, self).populate(req, ctx)
414
 
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
415
 
        ctx['semesters'] = req.store.find(Semester).order_by(
416
 
            Semester.year, Semester.semester)
417
 
        ctx['force_subject'] = None
418
 
 
419
 
    def populate_state(self, state):
420
 
        state.existing_offering = self.context
421
 
 
422
 
    def get_default_data(self, req):
423
 
        return {
424
 
            'subject': self.context.subject.short_name,
425
 
            'semester': self.context.semester.year + '/' +
426
 
                        self.context.semester.semester,
427
 
            'url': self.context.url,
428
 
            'description': self.context.description,
429
 
            'show_worksheet_marks': self.context.show_worksheet_marks,
430
 
            }
431
 
 
432
 
    def save_object(self, req, data):
433
 
        if req.user.admin:
434
 
            self.context.subject = data['subject']
435
 
            self.context.semester = data['semester']
436
 
        self.context.description = data['description']
437
 
        self.context.url = unicode(data['url']) if data['url'] else None
438
 
        self.context.show_worksheet_marks = data['show_worksheet_marks']
439
 
        return self.context
440
 
 
441
 
 
442
 
class OfferingNew(BaseFormView):
443
 
    """A form to create an offering."""
444
 
    template = 'templates/offering-new.html'
445
 
    tab = 'subjects'
446
 
 
447
 
    def authorize(self, req):
448
 
        return req.user is not None and req.user.admin
449
 
 
450
 
    @property
451
 
    def validator(self):
452
 
        return OfferingAdminSchema()
453
 
 
454
 
    def populate(self, req, ctx):
455
 
        super(OfferingNew, self).populate(req, ctx)
456
 
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
457
 
        ctx['semesters'] = req.store.find(Semester).order_by(
458
 
            Semester.year, Semester.semester)
459
 
        ctx['force_subject'] = None
460
 
 
461
 
    def populate_state(self, state):
462
 
        state.existing_offering = None
463
 
 
464
 
    def get_default_data(self, req):
465
 
        return {}
466
 
 
467
 
    def save_object(self, req, data):
468
 
        new_offering = Offering()
469
 
        new_offering.subject = data['subject']
470
 
        new_offering.semester = data['semester']
471
 
        new_offering.description = data['description']
472
 
        new_offering.url = unicode(data['url']) if data['url'] else None
473
 
        new_offering.show_worksheet_marks = data['show_worksheet_marks']
474
 
 
475
 
        req.store.add(new_offering)
476
 
        return new_offering
477
 
 
478
 
class SubjectOfferingNew(OfferingNew):
479
 
    """A form to create an offering for a given subject."""
480
 
    # Identical to OfferingNew, except it forces the subject to be the subject
481
 
    # in context
482
 
    def populate(self, req, ctx):
483
 
        super(SubjectOfferingNew, self).populate(req, ctx)
484
 
        ctx['force_subject'] = self.context
485
 
 
486
 
class OfferingCloneWorksheetsSchema(formencode.Schema):
487
 
    subject = formencode.All(
488
 
        SubjectValidator(), formencode.validators.UnicodeString())
489
 
    semester = formencode.All(
490
 
        SemesterValidator(), formencode.validators.UnicodeString())
491
 
 
492
 
 
493
 
class OfferingCloneWorksheets(BaseFormView):
494
 
    """A form to clone worksheets from one offering to another."""
495
 
    template = 'templates/offering-clone-worksheets.html'
496
 
    tab = 'subjects'
497
 
 
498
 
    def authorize(self, req):
499
 
        return req.user is not None and req.user.admin
500
 
 
501
 
    @property
502
 
    def validator(self):
503
 
        return OfferingCloneWorksheetsSchema()
504
 
 
505
 
    def populate(self, req, ctx):
506
 
        super(OfferingCloneWorksheets, self).populate(req, ctx)
507
 
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
508
 
        ctx['semesters'] = req.store.find(Semester).order_by(
509
 
            Semester.year, Semester.semester)
510
 
 
511
 
    def get_default_data(self, req):
512
 
        return {}
513
 
 
514
 
    def save_object(self, req, data):
515
 
        if self.context.worksheets.count() > 0:
516
 
            raise BadRequest(
517
 
                "Cannot clone to target with existing worksheets.")
518
 
        offering = req.store.find(
519
 
            Offering, subject=data['subject'], semester=data['semester']).one()
520
 
        if offering is None:
521
 
            raise BadRequest("No such offering.")
522
 
        if offering.worksheets.count() == 0:
523
 
            raise BadRequest("Source offering has no worksheets.")
524
 
 
525
 
        self.context.clone_worksheets(offering)
526
 
        return self.context
527
 
 
528
 
 
529
 
class UserValidator(formencode.FancyValidator):
530
 
    """A FormEncode validator that turns a username into a user.
531
 
 
532
 
    The state must have a 'store' attribute, which is the Storm store
533
 
    to use."""
534
 
    def _to_python(self, value, state):
535
 
        user = User.get_by_login(state.store, value)
536
 
        if user:
537
 
            return user
538
 
        else:
539
 
            raise formencode.Invalid('User does not exist', value, state)
540
 
 
541
 
 
542
 
class NoEnrolmentValidator(formencode.FancyValidator):
543
 
    """A FormEncode validator that ensures absence of an enrolment.
544
 
 
545
 
    The state must have an 'offering' attribute.
546
 
    """
547
 
    def _to_python(self, value, state):
548
 
        if state.offering.get_enrolment(value):
549
 
            raise formencode.Invalid('User already enrolled', value, state)
550
 
        return value
551
 
 
552
 
 
553
 
class RoleEnrolmentValidator(formencode.FancyValidator):
554
 
    """A FormEncode validator that checks permission to enrol users with a
555
 
    particular role.
556
 
 
557
 
    The state must have an 'offering' attribute.
558
 
    """
559
 
    def _to_python(self, value, state):
560
 
        if (("enrol_" + value) not in
561
 
                state.offering.get_permissions(state.user, state.config)):
562
 
            raise formencode.Invalid('Not allowed to assign users that role',
563
 
                                     value, state)
564
 
        return value
565
 
 
566
 
 
567
 
class EnrolSchema(formencode.Schema):
568
 
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
569
 
    role = formencode.All(formencode.validators.OneOf(
570
 
                                ["lecturer", "tutor", "student"]),
571
 
                          RoleEnrolmentValidator(),
572
 
                          formencode.validators.UnicodeString())
573
 
 
574
 
 
575
 
class EnrolmentsView(XHTMLView):
576
 
    """A page which displays all users enrolled in an offering."""
577
 
    template = 'templates/enrolments.html'
578
 
    tab = 'subjects'
579
 
    permission = 'edit'
580
 
    breadcrumb_text = 'Enrolments'
581
 
 
582
 
    def populate(self, req, ctx):
583
 
        ctx['req'] = req
584
 
        ctx['offering'] = self.context
585
 
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
586
 
        ctx['offering_perms'] = self.context.get_permissions(
587
 
            req.user, req.config)
588
 
        ctx['EnrolView'] = EnrolView
589
 
        ctx['EnrolmentEdit'] = EnrolmentEdit
590
 
        ctx['EnrolmentDelete'] = EnrolmentDelete
591
 
 
592
 
 
593
 
class EnrolView(XHTMLView):
594
 
    """A form to enrol a user in an offering."""
595
 
    template = 'templates/enrol.html'
596
 
    tab = 'subjects'
597
 
    permission = 'enrol'
598
 
 
599
 
    def filter(self, stream, ctx):
600
 
        return stream | HTMLFormFiller(data=ctx['data'])
601
 
 
602
 
    def populate(self, req, ctx):
603
 
        if req.method == 'POST':
604
 
            data = dict(req.get_fieldstorage())
605
 
            try:
606
 
                validator = EnrolSchema()
607
 
                req.offering = self.context # XXX: Getting into state.
608
 
                data = validator.to_python(data, state=req)
609
 
                self.context.enrol(data['user'], data['role'])
610
 
                req.store.commit()
611
 
                req.throw_redirect(req.uri)
612
 
            except formencode.Invalid, e:
613
 
                errors = e.unpack_errors()
614
 
        else:
615
 
            data = {}
616
 
            errors = {}
617
 
 
618
 
        ctx['data'] = data or {}
619
 
        ctx['offering'] = self.context
620
 
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
621
 
        ctx['errors'] = errors
622
 
        # If all of the fields validated, set the global form error.
623
 
        if isinstance(errors, basestring):
624
 
            ctx['error_value'] = errors
625
 
 
626
 
 
627
 
class EnrolmentEditSchema(formencode.Schema):
628
 
    role = formencode.All(formencode.validators.OneOf(
629
 
                                ["lecturer", "tutor", "student"]),
630
 
                          RoleEnrolmentValidator(),
631
 
                          formencode.validators.UnicodeString())
632
 
 
633
 
 
634
 
class EnrolmentEdit(BaseFormView):
635
 
    """A form to alter an enrolment's role."""
636
 
    template = 'templates/enrolment-edit.html'
637
 
    tab = 'subjects'
638
 
    permission = 'edit'
639
 
 
640
 
    def populate_state(self, state):
641
 
        state.offering = self.context.offering
642
 
 
643
 
    def get_default_data(self, req):
644
 
        return {'role': self.context.role}
645
 
 
646
 
    @property
647
 
    def validator(self):
648
 
        return EnrolmentEditSchema()
649
 
 
650
 
    def save_object(self, req, data):
651
 
        self.context.role = data['role']
652
 
 
653
 
    def get_return_url(self, obj):
654
 
        return self.req.publisher.generate(
655
 
            self.context.offering, EnrolmentsView)
656
 
 
657
 
    def populate(self, req, ctx):
658
 
        super(EnrolmentEdit, self).populate(req, ctx)
659
 
        ctx['offering_perms'] = self.context.offering.get_permissions(
660
 
            req.user, req.config)
661
 
 
662
 
 
663
 
class EnrolmentDelete(XHTMLView):
664
 
    """A form to alter an enrolment's role."""
665
 
    template = 'templates/enrolment-delete.html'
666
 
    tab = 'subjects'
667
 
    permission = 'edit'
668
 
 
669
 
    def populate(self, req, ctx):
670
 
        # If POSTing, delete delete delete.
671
 
        if req.method == 'POST':
672
 
            self.context.delete()
673
 
            req.store.commit()
674
 
            req.throw_redirect(req.publisher.generate(
675
 
                self.context.offering, EnrolmentsView))
676
 
 
677
 
        ctx['enrolment'] = self.context
678
 
 
679
 
 
680
 
class OfferingProjectsView(XHTMLView):
681
 
    """View the projects for an offering."""
682
 
    template = 'templates/offering_projects.html'
683
 
    permission = 'edit'
684
 
    tab = 'subjects'
685
 
    breadcrumb_text = 'Projects'
686
 
 
687
 
    def populate(self, req, ctx):
688
 
        self.plugin_styles[Plugin] = ["project.css"]
689
 
        ctx['req'] = req
690
 
        ctx['offering'] = self.context
691
 
        ctx['projectsets'] = []
692
 
 
693
 
        #Open the projectset Fragment, and render it for inclusion
694
 
        #into the ProjectSets page
695
 
        set_fragment = os.path.join(os.path.dirname(__file__),
696
 
                "templates/projectset_fragment.html")
697
 
        project_fragment = os.path.join(os.path.dirname(__file__),
698
 
                "templates/project_fragment.html")
699
 
 
700
 
        for projectset in \
701
 
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
702
 
            settmpl = self._loader.load(set_fragment)
703
 
            setCtx = Context()
704
 
            setCtx['req'] = req
705
 
            setCtx['projectset'] = projectset
706
 
            setCtx['projects'] = []
707
 
            setCtx['GroupsView'] = GroupsView
708
 
            setCtx['ProjectSetEdit'] = ProjectSetEdit
709
 
            setCtx['ProjectNew'] = ProjectNew
710
 
 
711
 
            for project in \
712
 
                projectset.projects.order_by(ivle.database.Project.deadline):
713
 
                projecttmpl = self._loader.load(project_fragment)
714
 
                projectCtx = Context()
715
 
                projectCtx['req'] = req
716
 
                projectCtx['project'] = project
717
 
                projectCtx['ProjectEdit'] = ProjectEdit
718
 
 
719
 
                setCtx['projects'].append(
720
 
                        projecttmpl.generate(projectCtx))
721
 
 
722
 
            ctx['projectsets'].append(settmpl.generate(setCtx))
723
 
 
724
 
 
725
 
class ProjectView(XHTMLView):
726
 
    """View the submissions for a ProjectSet"""
727
 
    template = "templates/project.html"
728
 
    permission = "view_project_submissions"
729
 
    tab = 'subjects'
730
 
 
731
 
    def build_subversion_url(self, svnroot, submission):
732
 
        princ = submission.assessed.principal
733
 
 
734
 
        if isinstance(princ, User):
735
 
            path = 'users/%s' % princ.login
736
 
        else:
737
 
            path = 'groups/%s_%s_%s_%s' % (
738
 
                    princ.project_set.offering.subject.short_name,
739
 
                    princ.project_set.offering.semester.year,
740
 
                    princ.project_set.offering.semester.semester,
741
 
                    princ.name
742
 
                    )
743
 
        return urlparse.urljoin(
744
 
                    svnroot,
745
 
                    os.path.join(path, submission.path[1:] if
746
 
                                       submission.path.startswith(os.sep) else
747
 
                                       submission.path))
748
 
 
749
 
    def populate(self, req, ctx):
750
 
        self.plugin_styles[Plugin] = ["project.css"]
751
 
 
752
 
        ctx['req'] = req
753
 
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
754
 
        ctx['GroupsView'] = GroupsView
755
 
        ctx['EnrolView'] = EnrolView
756
 
        ctx['format_datetime'] = ivle.date.make_date_nice
757
 
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
758
 
        ctx['build_subversion_url'] = self.build_subversion_url
759
 
        ctx['svn_addr'] = req.config['urls']['svn_addr']
760
 
        ctx['project'] = self.context
761
 
        ctx['user'] = req.user
762
 
        ctx['ProjectEdit'] = ProjectEdit
763
 
 
764
 
class ProjectUniquenessValidator(formencode.FancyValidator):
765
 
    """A FormEncode validator that checks that a project short_name is unique
766
 
    in a given offering.
767
 
 
768
 
    The project referenced by state.existing_project is permitted to
769
 
    hold that short_name. If any other project holds it, the input is rejected.
770
 
    """
771
 
    def _to_python(self, value, state):
772
 
        if (state.store.find(
773
 
            Project,
774
 
            Project.short_name == unicode(value),
775
 
            Project.project_set_id == ProjectSet.id,
776
 
            ProjectSet.offering == state.offering).one() not in
777
 
            (None, state.existing_project)):
778
 
            raise formencode.Invalid(
779
 
                "A project with that URL name already exists in this offering."
780
 
                , value, state)
781
 
        return value
782
 
 
783
 
class ProjectSchema(formencode.Schema):
784
 
    name = formencode.validators.UnicodeString(not_empty=True)
785
 
    short_name = formencode.All(
786
 
        URLNameValidator(not_empty=True),
787
 
        ProjectUniquenessValidator())
788
 
    deadline = DateTimeValidator(not_empty=True)
789
 
    url = formencode.validators.URL(if_missing=None, not_empty=False)
790
 
    synopsis = formencode.validators.UnicodeString(not_empty=True)
791
 
 
792
 
class ProjectEdit(BaseFormView):
793
 
    """A form to edit a project."""
794
 
    template = 'templates/project-edit.html'
795
 
    tab = 'subjects'
796
 
    permission = 'edit'
797
 
 
798
 
    @property
799
 
    def validator(self):
800
 
        return ProjectSchema()
801
 
 
802
 
    def populate(self, req, ctx):
803
 
        super(ProjectEdit, self).populate(req, ctx)
804
 
        ctx['projectset'] = self.context.project_set
805
 
 
806
 
    def populate_state(self, state):
807
 
        state.offering = self.context.project_set.offering
808
 
        state.existing_project = self.context
809
 
 
810
 
    def get_default_data(self, req):
811
 
        return {
812
 
            'name':         self.context.name,
813
 
            'short_name':   self.context.short_name,
814
 
            'deadline':     self.context.deadline,
815
 
            'url':          self.context.url,
816
 
            'synopsis':     self.context.synopsis,
817
 
            }
818
 
 
819
 
    def save_object(self, req, data):
820
 
        self.context.name = data['name']
821
 
        self.context.short_name = data['short_name']
822
 
        self.context.deadline = data['deadline']
823
 
        self.context.url = unicode(data['url']) if data['url'] else None
824
 
        self.context.synopsis = data['synopsis']
825
 
        return self.context
826
 
 
827
 
class ProjectNew(BaseFormView):
828
 
    """A form to create a new project."""
829
 
    template = 'templates/project-new.html'
830
 
    tab = 'subjects'
831
 
    permission = 'edit'
832
 
 
833
 
    @property
834
 
    def validator(self):
835
 
        return ProjectSchema()
836
 
 
837
 
    def populate(self, req, ctx):
838
 
        super(ProjectNew, self).populate(req, ctx)
839
 
        ctx['projectset'] = self.context
840
 
 
841
 
    def populate_state(self, state):
842
 
        state.offering = self.context.offering
843
 
        state.existing_project = None
844
 
 
845
 
    def get_default_data(self, req):
846
 
        return {}
847
 
 
848
 
    def save_object(self, req, data):
849
 
        new_project = Project()
850
 
        new_project.project_set = self.context
851
 
        new_project.name = data['name']
852
 
        new_project.short_name = data['short_name']
853
 
        new_project.deadline = data['deadline']
854
 
        new_project.url = unicode(data['url']) if data['url'] else None
855
 
        new_project.synopsis = data['synopsis']
856
 
        req.store.add(new_project)
857
 
        return new_project
858
 
 
859
 
class ProjectSetSchema(formencode.Schema):
860
 
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
861
 
 
862
 
class ProjectSetEdit(BaseFormView):
863
 
    """A form to edit a project set."""
864
 
    template = 'templates/projectset-edit.html'
865
 
    tab = 'subjects'
866
 
    permission = 'edit'
867
 
 
868
 
    @property
869
 
    def validator(self):
870
 
        return ProjectSetSchema()
871
 
 
872
 
    def populate(self, req, ctx):
873
 
        super(ProjectSetEdit, self).populate(req, ctx)
874
 
 
875
 
    def get_default_data(self, req):
876
 
        return {
877
 
            'group_size': self.context.max_students_per_group,
878
 
            }
879
 
 
880
 
    def save_object(self, req, data):
881
 
        self.context.max_students_per_group = data['group_size']
882
 
        return self.context
883
 
 
884
 
class ProjectSetNew(BaseFormView):
885
 
    """A form to create a new project set."""
886
 
    template = 'templates/projectset-new.html'
887
 
    tab = 'subjects'
888
 
    permission = 'edit'
889
 
    breadcrumb_text = "Projects"
890
 
 
891
 
    @property
892
 
    def validator(self):
893
 
        return ProjectSetSchema()
894
 
 
895
 
    def populate(self, req, ctx):
896
 
        super(ProjectSetNew, self).populate(req, ctx)
897
 
 
898
 
    def get_default_data(self, req):
899
 
        return {}
900
 
 
901
 
    def save_object(self, req, data):
902
 
        new_set = ProjectSet()
903
 
        new_set.offering = self.context
904
 
        new_set.max_students_per_group = data['group_size']
905
 
        req.store.add(new_set)
906
 
        return new_set
907
 
 
908
 
class Plugin(ViewPlugin, MediaPlugin):
909
 
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
910
 
                      offering_to_project, offering_to_projectset,
911
 
                      offering_to_enrolment)
912
 
    reverse_routes = (
913
 
        subject_url, semester_url, offering_url, projectset_url, project_url,
914
 
        enrolment_url)
915
 
 
916
 
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
917
 
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
918
 
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
919
 
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
920
 
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
921
 
             (Subject, '+index', SubjectView),
922
 
             (Subject, '+edit', SubjectEdit),
923
 
             (Subject, '+new-offering', SubjectOfferingNew),
924
 
             (Semester, '+edit', SemesterEdit),
925
 
             (Offering, '+index', OfferingView),
926
 
             (Offering, '+edit', OfferingEdit),
927
 
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
928
 
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
929
 
             (Offering, ('+enrolments', '+new'), EnrolView),
930
 
             (Enrolment, '+edit', EnrolmentEdit),
931
 
             (Enrolment, '+delete', EnrolmentDelete),
932
 
             (Offering, ('+projects', '+index'), OfferingProjectsView),
933
 
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
934
 
             (ProjectSet, '+edit', ProjectSetEdit),
935
 
             (ProjectSet, '+new', ProjectNew),
936
 
             (Project, '+index', ProjectView),
937
 
             (Project, '+edit', ProjectEdit),
938
 
             ]
939
 
 
940
 
    breadcrumbs = {Subject: SubjectBreadcrumb,
941
 
                   Offering: OfferingBreadcrumb,
942
 
                   User: UserBreadcrumb,
943
 
                   Project: ProjectBreadcrumb,
944
 
                   Enrolment: EnrolmentBreadcrumb,
945
 
                   }
946
 
 
947
 
    tabs = [
948
 
        ('subjects', 'Subjects',
949
 
         'View subject content and complete worksheets',
950
 
         'subjects.png', 'subjects', 5)
951
 
    ]
952
 
 
953
 
    media = 'subject-media'