~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: 2009-12-08 11:43:33 UTC
  • Revision ID: matt.giuca@gmail.com-20091208114333-wjrcukroelgh6m06
Enrolment page: Added a full stop after error messages (confusing when there are several).

Show diffs side-by-side

added added

removed removed

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