~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: 2009-04-28 06:55:03 UTC
  • Revision ID: grantw@unimelb.edu.au-20090428065503-w4me24f26s3fzb7a
Drop ivle.util.make_path (replaced by Request.make_path) and fix docstrings.

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