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

« back to all changes in this revision

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

  • Committer: stevenbird
  • Date: 2008-02-04 04:22:44 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:400
first commit of homepage at ivle.sourceforge.net, including screenshots; just web-presence at this stage

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