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

« back to all changes in this revision

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

  • Committer: Matt Giuca
  • Date: 2010-07-21 04:12:30 UTC
  • Revision ID: matt.giuca@gmail.com-20100721041230-hlrn2q3fx2uvaprd
ivle.webapp.admin.user: Fixed call to req.user.get_svn_url (now takes 1 argument, as of r1810).
Fixes Internal server error on Settings page (since r1810).

Show diffs side-by-side

added added

removed removed

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