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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2010-02-23 08:55:42 UTC
  • mto: This revision was merged to the branch mainline in revision 1674.
  • Revision ID: grantw@unimelb.edu.au-20100223085542-r8xw14bxxoraza51
Permit underscores in all names.

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
30
 
29
 
from storm.locals import Desc
 
31
from storm.locals import Desc, Store
 
32
import genshi
 
33
from genshi.filters import HTMLFormFiller
 
34
from genshi.template import Context, TemplateLoader
 
35
import formencode
 
36
import formencode.validators
30
37
 
 
38
from ivle.webapp.base.forms import BaseFormView, URLNameValidator
 
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
40
from ivle.webapp.base.xhtml import XHTMLView
32
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
33
 
from ivle.webapp.errors import NotFound
34
 
from ivle.database import Subject, Semester
 
41
from ivle.webapp.errors import BadRequest
 
42
from ivle.webapp import ApplicationRoot
 
43
 
 
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
45
                          ProjectSet, Project, ProjectSubmission
35
46
from ivle import util
 
47
import ivle.date
36
48
 
 
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
 
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
 
52
            subject_to_offering, offering_to_projectset, offering_to_project,
 
53
            offering_to_enrolment, subject_url, semester_url, offering_url,
 
54
            projectset_url, project_url, enrolment_url)
 
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
 
56
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
 
57
            EnrolmentBreadcrumb)
 
58
from ivle.webapp.core import Plugin as CorePlugin
 
59
from ivle.webapp.groups import GroupsView
 
60
from ivle.webapp.media import media_url
 
61
from ivle.webapp.tutorial import Plugin as TutorialPlugin
37
62
 
38
63
class SubjectsView(XHTMLView):
39
64
    '''The view of the list of subjects.'''
40
 
    template = 'subjects.html'
 
65
    template = 'templates/subjects.html'
41
66
    tab = 'subjects'
42
67
 
43
68
    def authorize(self, req):
44
69
        return req.user is not None
45
70
 
46
71
    def populate(self, req, ctx):
 
72
        ctx['req'] = req
 
73
        ctx['user'] = req.user
47
74
        ctx['semesters'] = []
 
75
 
48
76
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
49
77
                                                     Desc(Semester.semester)):
50
 
            enrolments = semester.enrolments.find(user=req.user)
51
 
            if enrolments.count():
52
 
                ctx['semesters'].append((semester, enrolments))
 
78
            if req.user.admin:
 
79
                # For admins, show all subjects in the system
 
80
                offerings = list(semester.offerings.find())
 
81
            else:
 
82
                offerings = [enrolment.offering for enrolment in
 
83
                                    semester.enrolments.find(user=req.user)]
 
84
            if len(offerings):
 
85
                ctx['semesters'].append((semester, offerings))
 
86
 
 
87
 
 
88
class SubjectsManage(XHTMLView):
 
89
    '''Subject management view.'''
 
90
    template = 'templates/subjects-manage.html'
 
91
    tab = 'subjects'
 
92
 
 
93
    def authorize(self, req):
 
94
        return req.user is not None and req.user.admin
 
95
 
 
96
    def populate(self, req, ctx):
 
97
        ctx['req'] = req
 
98
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
99
        ctx['SubjectEdit'] = SubjectEdit
 
100
        ctx['SemesterEdit'] = SemesterEdit
 
101
 
 
102
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
103
        ctx['semesters'] = req.store.find(Semester).order_by(
 
104
            Semester.year, Semester.semester)
 
105
 
 
106
 
 
107
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
108
    """A FormEncode validator that checks that a subject name is unused.
 
109
 
 
110
    The subject referenced by state.existing_subject is permitted
 
111
    to hold that name. If any other object holds it, the input is rejected.
 
112
    """
 
113
    def __init__(self, matching=None):
 
114
        self.matching = matching
 
115
 
 
116
    def _to_python(self, value, state):
 
117
        if (state.store.find(
 
118
                Subject, short_name=value).one() not in
 
119
                (None, state.existing_subject)):
 
120
            raise formencode.Invalid(
 
121
                'Short name already taken', value, state)
 
122
        return value
 
123
 
 
124
 
 
125
class SubjectSchema(formencode.Schema):
 
126
    short_name = formencode.All(
 
127
        SubjectShortNameUniquenessValidator(),
 
128
        URLNameValidator(not_empty=True))
 
129
    name = formencode.validators.UnicodeString(not_empty=True)
 
130
    code = formencode.validators.UnicodeString(not_empty=True)
 
131
 
 
132
 
 
133
class SubjectFormView(BaseFormView):
 
134
    """An abstract form to add or edit a subject."""
 
135
    tab = 'subjects'
 
136
 
 
137
    def authorize(self, req):
 
138
        return req.user is not None and req.user.admin
 
139
 
 
140
    def populate_state(self, state):
 
141
        state.existing_subject = None
 
142
 
 
143
    @property
 
144
    def validator(self):
 
145
        return SubjectSchema()
 
146
 
 
147
    def get_return_url(self, obj):
 
148
        return '/subjects'
 
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
 
 
270
class OfferingView(XHTMLView):
 
271
    """The home page of an offering."""
 
272
    template = 'templates/offering.html'
 
273
    tab = 'subjects'
 
274
    permission = 'view'
 
275
 
 
276
    def populate(self, req, ctx):
 
277
        # Need the worksheet result styles.
 
278
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
279
        ctx['context'] = self.context
 
280
        ctx['req'] = req
 
281
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
282
        ctx['format_submission_principal'] = util.format_submission_principal
 
283
        ctx['format_datetime'] = ivle.date.make_date_nice
 
284
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
285
        ctx['OfferingEdit'] = OfferingEdit
 
286
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
 
287
        ctx['GroupsView'] = GroupsView
 
288
        ctx['EnrolmentsView'] = EnrolmentsView
 
289
 
 
290
        # As we go, calculate the total score for this subject
 
291
        # (Assessable worksheets only, mandatory problems only)
 
292
 
 
293
        ctx['worksheets'], problems_total, problems_done = (
 
294
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
295
                req.store, req.user, self.context))
 
296
 
 
297
        ctx['exercises_total'] = problems_total
 
298
        ctx['exercises_done'] = problems_done
 
299
        if problems_total > 0:
 
300
            if problems_done >= problems_total:
 
301
                ctx['worksheets_complete_class'] = "complete"
 
302
            elif problems_done > 0:
 
303
                ctx['worksheets_complete_class'] = "semicomplete"
 
304
            else:
 
305
                ctx['worksheets_complete_class'] = "incomplete"
 
306
            # Calculate the final percentage and mark for the subject
 
307
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
308
             ctx['worksheet_max_mark']) = (
 
309
                ivle.worksheet.utils.calculate_mark(
 
310
                    problems_done, problems_total))
 
311
 
 
312
 
 
313
class SubjectValidator(formencode.FancyValidator):
 
314
    """A FormEncode validator that turns a subject name into a subject.
 
315
 
 
316
    The state must have a 'store' attribute, which is the Storm store
 
317
    to use.
 
318
    """
 
319
    def _to_python(self, value, state):
 
320
        subject = state.store.find(Subject, short_name=value).one()
 
321
        if subject:
 
322
            return subject
 
323
        else:
 
324
            raise formencode.Invalid('Subject does not exist', value, state)
 
325
 
 
326
 
 
327
class SemesterValidator(formencode.FancyValidator):
 
328
    """A FormEncode validator that turns a string into a semester.
 
329
 
 
330
    The string should be of the form 'year/semester', eg. '2009/1'.
 
331
 
 
332
    The state must have a 'store' attribute, which is the Storm store
 
333
    to use.
 
334
    """
 
335
    def _to_python(self, value, state):
 
336
        try:
 
337
            year, semester = value.split('/')
 
338
        except ValueError:
 
339
            year = semester = None
 
340
 
 
341
        semester = state.store.find(
 
342
            Semester, year=year, semester=semester).one()
 
343
        if semester:
 
344
            return semester
 
345
        else:
 
346
            raise formencode.Invalid('Semester does not exist', value, state)
 
347
 
 
348
 
 
349
class OfferingUniquenessValidator(formencode.FancyValidator):
 
350
    """A FormEncode validator that checks that an offering is unique.
 
351
 
 
352
    There cannot be more than one offering in the same year and semester.
 
353
 
 
354
    The offering referenced by state.existing_offering is permitted to
 
355
    hold that year and semester tuple. If any other object holds it, the
 
356
    input is rejected.
 
357
    """
 
358
    def _to_python(self, value, state):
 
359
        if (state.store.find(
 
360
                Offering, subject=value['subject'],
 
361
                semester=value['semester']).one() not in
 
362
                (None, state.existing_offering)):
 
363
            raise formencode.Invalid(
 
364
                'Offering already exists', value, state)
 
365
        return value
 
366
 
 
367
 
 
368
class OfferingSchema(formencode.Schema):
 
369
    description = formencode.validators.UnicodeString(
 
370
        if_missing=None, not_empty=False)
 
371
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
372
 
 
373
 
 
374
class OfferingAdminSchema(OfferingSchema):
 
375
    subject = formencode.All(
 
376
        SubjectValidator(), formencode.validators.UnicodeString())
 
377
    semester = formencode.All(
 
378
        SemesterValidator(), formencode.validators.UnicodeString())
 
379
    chained_validators = [OfferingUniquenessValidator()]
 
380
 
 
381
 
 
382
class OfferingEdit(BaseFormView):
 
383
    """A form to edit an offering's details."""
 
384
    template = 'templates/offering-edit.html'
 
385
    tab = 'subjects'
 
386
    permission = 'edit'
 
387
 
 
388
    @property
 
389
    def validator(self):
 
390
        if self.req.user.admin:
 
391
            return OfferingAdminSchema()
 
392
        else:
 
393
            return OfferingSchema()
 
394
 
 
395
    def populate(self, req, ctx):
 
396
        super(OfferingEdit, self).populate(req, ctx)
 
397
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
398
        ctx['semesters'] = req.store.find(Semester).order_by(
 
399
            Semester.year, Semester.semester)
 
400
 
 
401
    def populate_state(self, state):
 
402
        state.existing_offering = self.context
 
403
 
 
404
    def get_default_data(self, req):
 
405
        return {
 
406
            'subject': self.context.subject.short_name,
 
407
            'semester': self.context.semester.year + '/' +
 
408
                        self.context.semester.semester,
 
409
            'url': self.context.url,
 
410
            'description': self.context.description,
 
411
            }
 
412
 
 
413
    def save_object(self, req, data):
 
414
        if req.user.admin:
 
415
            self.context.subject = data['subject']
 
416
            self.context.semester = data['semester']
 
417
        self.context.description = data['description']
 
418
        self.context.url = unicode(data['url']) if data['url'] else None
 
419
        return self.context
 
420
 
 
421
 
 
422
class OfferingNew(BaseFormView):
 
423
    """A form to create an offering."""
 
424
    template = 'templates/offering-new.html'
 
425
    tab = 'subjects'
 
426
 
 
427
    def authorize(self, req):
 
428
        return req.user is not None and req.user.admin
 
429
 
 
430
    @property
 
431
    def validator(self):
 
432
        return OfferingAdminSchema()
 
433
 
 
434
    def populate(self, req, ctx):
 
435
        super(OfferingNew, self).populate(req, ctx)
 
436
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
437
        ctx['semesters'] = req.store.find(Semester).order_by(
 
438
            Semester.year, Semester.semester)
 
439
 
 
440
    def populate_state(self, state):
 
441
        state.existing_offering = None
 
442
 
 
443
    def get_default_data(self, req):
 
444
        return {}
 
445
 
 
446
    def save_object(self, req, data):
 
447
        new_offering = Offering()
 
448
        new_offering.subject = data['subject']
 
449
        new_offering.semester = data['semester']
 
450
        new_offering.description = data['description']
 
451
        new_offering.url = unicode(data['url']) if data['url'] else None
 
452
 
 
453
        req.store.add(new_offering)
 
454
        return new_offering
 
455
 
 
456
 
 
457
class OfferingCloneWorksheetsSchema(formencode.Schema):
 
458
    subject = formencode.All(
 
459
        SubjectValidator(), formencode.validators.UnicodeString())
 
460
    semester = formencode.All(
 
461
        SemesterValidator(), formencode.validators.UnicodeString())
 
462
 
 
463
 
 
464
class OfferingCloneWorksheets(BaseFormView):
 
465
    """A form to clone worksheets from one offering to another."""
 
466
    template = 'templates/offering-clone-worksheets.html'
 
467
    tab = 'subjects'
 
468
 
 
469
    def authorize(self, req):
 
470
        return req.user is not None and req.user.admin
 
471
 
 
472
    @property
 
473
    def validator(self):
 
474
        return OfferingCloneWorksheetsSchema()
 
475
 
 
476
    def populate(self, req, ctx):
 
477
        super(OfferingCloneWorksheets, self).populate(req, ctx)
 
478
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
479
        ctx['semesters'] = req.store.find(Semester).order_by(
 
480
            Semester.year, Semester.semester)
 
481
 
 
482
    def get_default_data(self, req):
 
483
        return {}
 
484
 
 
485
    def save_object(self, req, data):
 
486
        if self.context.worksheets.count() > 0:
 
487
            raise BadRequest(
 
488
                "Cannot clone to target with existing worksheets.")
 
489
        offering = req.store.find(
 
490
            Offering, subject=data['subject'], semester=data['semester']).one()
 
491
        if offering is None:
 
492
            raise BadRequest("No such offering.")
 
493
        if offering.worksheets.count() == 0:
 
494
            raise BadRequest("Source offering has no worksheets.")
 
495
 
 
496
        self.context.clone_worksheets(offering)
 
497
        return self.context
 
498
 
 
499
 
 
500
class UserValidator(formencode.FancyValidator):
 
501
    """A FormEncode validator that turns a username into a user.
 
502
 
 
503
    The state must have a 'store' attribute, which is the Storm store
 
504
    to use."""
 
505
    def _to_python(self, value, state):
 
506
        user = User.get_by_login(state.store, value)
 
507
        if user:
 
508
            return user
 
509
        else:
 
510
            raise formencode.Invalid('User does not exist', value, state)
 
511
 
 
512
 
 
513
class NoEnrolmentValidator(formencode.FancyValidator):
 
514
    """A FormEncode validator that ensures absence of an enrolment.
 
515
 
 
516
    The state must have an 'offering' attribute.
 
517
    """
 
518
    def _to_python(self, value, state):
 
519
        if state.offering.get_enrolment(value):
 
520
            raise formencode.Invalid('User already enrolled', value, state)
 
521
        return value
 
522
 
 
523
 
 
524
class RoleEnrolmentValidator(formencode.FancyValidator):
 
525
    """A FormEncode validator that checks permission to enrol users with a
 
526
    particular role.
 
527
 
 
528
    The state must have an 'offering' attribute.
 
529
    """
 
530
    def _to_python(self, value, state):
 
531
        if (("enrol_" + value) not in
 
532
                state.offering.get_permissions(state.user, state.config)):
 
533
            raise formencode.Invalid('Not allowed to assign users that role',
 
534
                                     value, state)
 
535
        return value
 
536
 
 
537
 
 
538
class EnrolSchema(formencode.Schema):
 
539
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
540
    role = formencode.All(formencode.validators.OneOf(
 
541
                                ["lecturer", "tutor", "student"]),
 
542
                          RoleEnrolmentValidator(),
 
543
                          formencode.validators.UnicodeString())
 
544
 
 
545
 
 
546
class EnrolmentsView(XHTMLView):
 
547
    """A page which displays all users enrolled in an offering."""
 
548
    template = 'templates/enrolments.html'
 
549
    tab = 'subjects'
 
550
    permission = 'edit'
 
551
    breadcrumb_text = 'Enrolments'
 
552
 
 
553
    def populate(self, req, ctx):
 
554
        ctx['req'] = req
 
555
        ctx['offering'] = self.context
 
556
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
557
        ctx['offering_perms'] = self.context.get_permissions(
 
558
            req.user, req.config)
 
559
        ctx['EnrolView'] = EnrolView
 
560
        ctx['EnrolmentEdit'] = EnrolmentEdit
 
561
        ctx['EnrolmentDelete'] = EnrolmentDelete
 
562
 
 
563
 
 
564
class EnrolView(XHTMLView):
 
565
    """A form to enrol a user in an offering."""
 
566
    template = 'templates/enrol.html'
 
567
    tab = 'subjects'
 
568
    permission = 'enrol'
 
569
 
 
570
    def filter(self, stream, ctx):
 
571
        return stream | HTMLFormFiller(data=ctx['data'])
 
572
 
 
573
    def populate(self, req, ctx):
 
574
        if req.method == 'POST':
 
575
            data = dict(req.get_fieldstorage())
 
576
            try:
 
577
                validator = EnrolSchema()
 
578
                req.offering = self.context # XXX: Getting into state.
 
579
                data = validator.to_python(data, state=req)
 
580
                self.context.enrol(data['user'], data['role'])
 
581
                req.store.commit()
 
582
                req.throw_redirect(req.uri)
 
583
            except formencode.Invalid, e:
 
584
                errors = e.unpack_errors()
 
585
        else:
 
586
            data = {}
 
587
            errors = {}
 
588
 
 
589
        ctx['data'] = data or {}
 
590
        ctx['offering'] = self.context
 
591
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
 
592
        ctx['errors'] = errors
 
593
 
 
594
 
 
595
class EnrolmentEditSchema(formencode.Schema):
 
596
    role = formencode.All(formencode.validators.OneOf(
 
597
                                ["lecturer", "tutor", "student"]),
 
598
                          RoleEnrolmentValidator(),
 
599
                          formencode.validators.UnicodeString())
 
600
 
 
601
 
 
602
class EnrolmentEdit(BaseFormView):
 
603
    """A form to alter an enrolment's role."""
 
604
    template = 'templates/enrolment-edit.html'
 
605
    tab = 'subjects'
 
606
    permission = 'edit'
 
607
 
 
608
    def populate_state(self, state):
 
609
        state.offering = self.context.offering
 
610
 
 
611
    def get_default_data(self, req):
 
612
        return {'role': self.context.role}
 
613
 
 
614
    @property
 
615
    def validator(self):
 
616
        return EnrolmentEditSchema()
 
617
 
 
618
    def save_object(self, req, data):
 
619
        self.context.role = data['role']
 
620
 
 
621
    def get_return_url(self, obj):
 
622
        return self.req.publisher.generate(
 
623
            self.context.offering, EnrolmentsView)
 
624
 
 
625
    def populate(self, req, ctx):
 
626
        super(EnrolmentEdit, self).populate(req, ctx)
 
627
        ctx['offering_perms'] = self.context.offering.get_permissions(
 
628
            req.user, req.config)
 
629
 
 
630
 
 
631
class EnrolmentDelete(XHTMLView):
 
632
    """A form to alter an enrolment's role."""
 
633
    template = 'templates/enrolment-delete.html'
 
634
    tab = 'subjects'
 
635
    permission = 'edit'
 
636
 
 
637
    def populate(self, req, ctx):
 
638
        # If POSTing, delete delete delete.
 
639
        if req.method == 'POST':
 
640
            self.context.delete()
 
641
            req.store.commit()
 
642
            req.throw_redirect(req.publisher.generate(
 
643
                self.context.offering, EnrolmentsView))
 
644
 
 
645
        ctx['enrolment'] = self.context
 
646
 
 
647
 
 
648
class OfferingProjectsView(XHTMLView):
 
649
    """View the projects for an offering."""
 
650
    template = 'templates/offering_projects.html'
 
651
    permission = 'edit'
 
652
    tab = 'subjects'
 
653
    breadcrumb_text = 'Projects'
 
654
 
 
655
    def populate(self, req, ctx):
 
656
        self.plugin_styles[Plugin] = ["project.css"]
 
657
        self.plugin_scripts[Plugin] = ["project.js"]
 
658
        ctx['req'] = req
 
659
        ctx['offering'] = self.context
 
660
        ctx['projectsets'] = []
 
661
        ctx['OfferingRESTView'] = OfferingRESTView
 
662
 
 
663
        #Open the projectset Fragment, and render it for inclusion
 
664
        #into the ProjectSets page
 
665
        #XXX: This could be a lot cleaner
 
666
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
667
 
 
668
        set_fragment = os.path.join(os.path.dirname(__file__),
 
669
                "templates/projectset_fragment.html")
 
670
        project_fragment = os.path.join(os.path.dirname(__file__),
 
671
                "templates/project_fragment.html")
 
672
 
 
673
        for projectset in self.context.project_sets:
 
674
            settmpl = loader.load(set_fragment)
 
675
            setCtx = Context()
 
676
            setCtx['req'] = req
 
677
            setCtx['projectset'] = projectset
 
678
            setCtx['projects'] = []
 
679
            setCtx['GroupsView'] = GroupsView
 
680
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
681
 
 
682
            for project in projectset.projects:
 
683
                projecttmpl = loader.load(project_fragment)
 
684
                projectCtx = Context()
 
685
                projectCtx['req'] = req
 
686
                projectCtx['project'] = project
 
687
 
 
688
                setCtx['projects'].append(
 
689
                        projecttmpl.generate(projectCtx))
 
690
 
 
691
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
692
 
 
693
 
 
694
class ProjectView(XHTMLView):
 
695
    """View the submissions for a ProjectSet"""
 
696
    template = "templates/project.html"
 
697
    permission = "view_project_submissions"
 
698
    tab = 'subjects'
 
699
 
 
700
    def build_subversion_url(self, svnroot, submission):
 
701
        princ = submission.assessed.principal
 
702
 
 
703
        if isinstance(princ, User):
 
704
            path = 'users/%s' % princ.login
 
705
        else:
 
706
            path = 'groups/%s_%s_%s_%s' % (
 
707
                    princ.project_set.offering.subject.short_name,
 
708
                    princ.project_set.offering.semester.year,
 
709
                    princ.project_set.offering.semester.semester,
 
710
                    princ.name
 
711
                    )
 
712
        return urlparse.urljoin(
 
713
                    svnroot,
 
714
                    os.path.join(path, submission.path[1:] if
 
715
                                       submission.path.startswith(os.sep) else
 
716
                                       submission.path))
 
717
 
 
718
    def populate(self, req, ctx):
 
719
        self.plugin_styles[Plugin] = ["project.css"]
 
720
 
 
721
        ctx['req'] = req
 
722
        ctx['GroupsView'] = GroupsView
 
723
        ctx['EnrolView'] = EnrolView
 
724
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
725
        ctx['build_subversion_url'] = self.build_subversion_url
 
726
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
727
        ctx['project'] = self.context
 
728
        ctx['user'] = req.user
53
729
 
54
730
class Plugin(ViewPlugin, MediaPlugin):
55
 
    urls = [
56
 
        ('subjects/', SubjectsView),
57
 
    ]
 
731
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
 
732
                      offering_to_project, offering_to_projectset,
 
733
                      offering_to_enrolment)
 
734
    reverse_routes = (
 
735
        subject_url, semester_url, offering_url, projectset_url, project_url,
 
736
        enrolment_url)
 
737
 
 
738
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
739
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
 
740
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
741
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
 
742
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
 
743
             (Subject, '+edit', SubjectEdit),
 
744
             (Semester, '+edit', SemesterEdit),
 
745
             (Offering, '+index', OfferingView),
 
746
             (Offering, '+edit', OfferingEdit),
 
747
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
 
748
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
749
             (Offering, ('+enrolments', '+new'), EnrolView),
 
750
             (Enrolment, '+edit', EnrolmentEdit),
 
751
             (Enrolment, '+delete', EnrolmentDelete),
 
752
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
753
             (Project, '+index', ProjectView),
 
754
 
 
755
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
756
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
757
             ]
 
758
 
 
759
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
760
                   Offering: OfferingBreadcrumb,
 
761
                   User: UserBreadcrumb,
 
762
                   Project: ProjectBreadcrumb,
 
763
                   Enrolment: EnrolmentBreadcrumb,
 
764
                   }
58
765
 
59
766
    tabs = [
60
767
        ('subjects', 'Subjects',