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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2010-07-30 01:16:11 UTC
  • Revision ID: grantw@unimelb.edu.au-20100730011611-ik9hj6yueeh6gc9h
Set version to 1.0.2rc1.

Show diffs side-by-side

added added

removed removed

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