~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-07-27 09:02:06 UTC
  • mto: This revision was merged to the branch mainline in revision 1824.
  • Revision ID: grantw@unimelb.edu.au-20100727090206-pmf5j6lu6xc892q8
Replace semester.semester with semester.{code,url_name,display_name}.

Show diffs side-by-side

added added

removed removed

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