~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-18 23:46:54 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:82
server/__init__: Rewrote with a better modular architecture.
    Now obeys white/blacklist and looks in conf to get the interpreter to use.
    (Note: Crufty execute_cgi is still present).

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