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

« back to all changes in this revision

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

  • Committer: Matt Giuca
  • Date: 2010-02-25 07:54:59 UTC
  • mto: This revision was merged to the branch mainline in revision 1731.
  • Revision ID: matt.giuca@gmail.com-20100225075459-ofwtu37q7d8rbmc8
Fully link the Project Delete view, from all the places the Project Edit view is linked.

Show diffs side-by-side

added added

removed removed

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