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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2010-02-25 09:10:36 UTC
  • Revision ID: grantw@unimelb.edu.au-20100225091036-urjqg7cbvvmlmd7s
Print a CPythonesque version on console startup.

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