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

« back to all changes in this revision

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

  • Committer: mattgiuca
  • Date: 2007-12-12 00:05:33 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:36
Added Makefile for building apps documentation.

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, req, submission):
739
 
        princ = submission.assessed.principal
740
 
 
741
 
        return os.path.join(princ.get_svn_url(req.config, req),
742
 
                            submission.path[1:] if
743
 
                                submission.path.startswith(os.sep) else
744
 
                                submission.path)
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['build_subversion_url'] = self.build_subversion_url
756
 
        ctx['project'] = self.context
757
 
        ctx['user'] = req.user
758
 
        ctx['ProjectEdit'] = ProjectEdit
759
 
        ctx['ProjectDelete'] = ProjectDelete
760
 
 
761
 
class ProjectUniquenessValidator(formencode.FancyValidator):
762
 
    """A FormEncode validator that checks that a project short_name is unique
763
 
    in a given offering.
764
 
 
765
 
    The project referenced by state.existing_project is permitted to
766
 
    hold that short_name. If any other project holds it, the input is rejected.
767
 
    """
768
 
    def _to_python(self, value, state):
769
 
        if (state.store.find(
770
 
            Project,
771
 
            Project.short_name == unicode(value),
772
 
            Project.project_set_id == ProjectSet.id,
773
 
            ProjectSet.offering == state.offering).one() not in
774
 
            (None, state.existing_project)):
775
 
            raise formencode.Invalid(
776
 
                "A project with that URL name already exists in this offering."
777
 
                , value, state)
778
 
        return value
779
 
 
780
 
class ProjectSchema(formencode.Schema):
781
 
    name = formencode.validators.UnicodeString(not_empty=True)
782
 
    short_name = formencode.All(
783
 
        URLNameValidator(not_empty=True),
784
 
        ProjectUniquenessValidator())
785
 
    deadline = DateTimeValidator(not_empty=True)
786
 
    url = formencode.validators.URL(if_missing=None, not_empty=False)
787
 
    synopsis = formencode.validators.UnicodeString(not_empty=True)
788
 
 
789
 
class ProjectEdit(BaseFormView):
790
 
    """A form to edit a project."""
791
 
    template = 'templates/project-edit.html'
792
 
    tab = 'subjects'
793
 
    permission = 'edit'
794
 
 
795
 
    @property
796
 
    def validator(self):
797
 
        return ProjectSchema()
798
 
 
799
 
    def populate(self, req, ctx):
800
 
        super(ProjectEdit, self).populate(req, ctx)
801
 
        ctx['projectset'] = self.context.project_set
802
 
 
803
 
    def populate_state(self, state):
804
 
        state.offering = self.context.project_set.offering
805
 
        state.existing_project = self.context
806
 
 
807
 
    def get_default_data(self, req):
808
 
        return {
809
 
            'name':         self.context.name,
810
 
            'short_name':   self.context.short_name,
811
 
            'deadline':     self.context.deadline,
812
 
            'url':          self.context.url,
813
 
            'synopsis':     self.context.synopsis,
814
 
            }
815
 
 
816
 
    def save_object(self, req, data):
817
 
        self.context.name = data['name']
818
 
        self.context.short_name = data['short_name']
819
 
        self.context.deadline = data['deadline']
820
 
        self.context.url = unicode(data['url']) if data['url'] else None
821
 
        self.context.synopsis = data['synopsis']
822
 
        return self.context
823
 
 
824
 
class ProjectNew(BaseFormView):
825
 
    """A form to create a new project."""
826
 
    template = 'templates/project-new.html'
827
 
    tab = 'subjects'
828
 
    permission = 'edit'
829
 
 
830
 
    @property
831
 
    def validator(self):
832
 
        return ProjectSchema()
833
 
 
834
 
    def populate(self, req, ctx):
835
 
        super(ProjectNew, self).populate(req, ctx)
836
 
        ctx['projectset'] = self.context
837
 
 
838
 
    def populate_state(self, state):
839
 
        state.offering = self.context.offering
840
 
        state.existing_project = None
841
 
 
842
 
    def get_default_data(self, req):
843
 
        return {}
844
 
 
845
 
    def save_object(self, req, data):
846
 
        new_project = Project()
847
 
        new_project.project_set = self.context
848
 
        new_project.name = data['name']
849
 
        new_project.short_name = data['short_name']
850
 
        new_project.deadline = data['deadline']
851
 
        new_project.url = unicode(data['url']) if data['url'] else None
852
 
        new_project.synopsis = data['synopsis']
853
 
        req.store.add(new_project)
854
 
        return new_project
855
 
 
856
 
class ProjectDelete(XHTMLView):
857
 
    """A form to delete a project."""
858
 
    template = 'templates/project-delete.html'
859
 
    tab = 'subjects'
860
 
    permission = 'edit'
861
 
 
862
 
    def populate(self, req, ctx):
863
 
        # If post, delete the project, or display a message explaining that
864
 
        # the project cannot be deleted
865
 
        if self.context.can_delete:
866
 
            if req.method == 'POST':
867
 
                self.context.delete()
868
 
                self.template = 'templates/project-deleted.html'
869
 
        else:
870
 
            # Can't delete
871
 
            self.template = 'templates/project-undeletable.html'
872
 
 
873
 
        # If get and can delete, display a delete confirmation page
874
 
 
875
 
        # Variables for the template
876
 
        ctx['req'] = req
877
 
        ctx['project'] = self.context
878
 
        ctx['OfferingProjectsView'] = OfferingProjectsView
879
 
 
880
 
class ProjectSetSchema(formencode.Schema):
881
 
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
882
 
 
883
 
class ProjectSetEdit(BaseFormView):
884
 
    """A form to edit a project set."""
885
 
    template = 'templates/projectset-edit.html'
886
 
    tab = 'subjects'
887
 
    permission = 'edit'
888
 
 
889
 
    @property
890
 
    def validator(self):
891
 
        return ProjectSetSchema()
892
 
 
893
 
    def populate(self, req, ctx):
894
 
        super(ProjectSetEdit, self).populate(req, ctx)
895
 
 
896
 
    def get_default_data(self, req):
897
 
        return {
898
 
            'group_size': self.context.max_students_per_group,
899
 
            }
900
 
 
901
 
    def save_object(self, req, data):
902
 
        self.context.max_students_per_group = data['group_size']
903
 
        return self.context
904
 
 
905
 
class ProjectSetNew(BaseFormView):
906
 
    """A form to create a new project set."""
907
 
    template = 'templates/projectset-new.html'
908
 
    tab = 'subjects'
909
 
    permission = 'edit'
910
 
    breadcrumb_text = "Projects"
911
 
 
912
 
    @property
913
 
    def validator(self):
914
 
        return ProjectSetSchema()
915
 
 
916
 
    def populate(self, req, ctx):
917
 
        super(ProjectSetNew, self).populate(req, ctx)
918
 
 
919
 
    def get_default_data(self, req):
920
 
        return {}
921
 
 
922
 
    def save_object(self, req, data):
923
 
        new_set = ProjectSet()
924
 
        new_set.offering = self.context
925
 
        new_set.max_students_per_group = data['group_size']
926
 
        req.store.add(new_set)
927
 
        return new_set
928
 
 
929
 
class Plugin(ViewPlugin, MediaPlugin):
930
 
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
931
 
                      offering_to_project, offering_to_projectset,
932
 
                      offering_to_enrolment)
933
 
    reverse_routes = (
934
 
        subject_url, semester_url, offering_url, projectset_url, project_url,
935
 
        enrolment_url)
936
 
 
937
 
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
938
 
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
939
 
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
940
 
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
941
 
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
942
 
             (Subject, '+index', SubjectView),
943
 
             (Subject, '+edit', SubjectEdit),
944
 
             (Subject, '+new-offering', SubjectOfferingNew),
945
 
             (Semester, '+edit', SemesterEdit),
946
 
             (Offering, '+index', OfferingView),
947
 
             (Offering, '+edit', OfferingEdit),
948
 
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
949
 
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
950
 
             (Offering, ('+enrolments', '+new'), EnrolView),
951
 
             (Enrolment, '+edit', EnrolmentEdit),
952
 
             (Enrolment, '+delete', EnrolmentDelete),
953
 
             (Offering, ('+projects', '+index'), OfferingProjectsView),
954
 
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
955
 
             (ProjectSet, '+edit', ProjectSetEdit),
956
 
             (ProjectSet, '+new', ProjectNew),
957
 
             (Project, '+index', ProjectView),
958
 
             (Project, '+edit', ProjectEdit),
959
 
             (Project, '+delete', ProjectDelete),
960
 
             ]
961
 
 
962
 
    breadcrumbs = {Subject: SubjectBreadcrumb,
963
 
                   Offering: OfferingBreadcrumb,
964
 
                   User: UserBreadcrumb,
965
 
                   Project: ProjectBreadcrumb,
966
 
                   Enrolment: EnrolmentBreadcrumb,
967
 
                   }
968
 
 
969
 
    tabs = [
970
 
        ('subjects', 'Subjects',
971
 
         'View subject content and complete worksheets',
972
 
         'subjects.png', 'subjects', 5)
973
 
    ]
974
 
 
975
 
    media = 'subject-media'