~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-03-22 06:05:32 UTC
  • Revision ID: matt.giuca@gmail.com-20100322060532-5365361xrx9mh32v
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.

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, TemplateLoader
 
34
from genshi.template import Context
35
35
import formencode
 
36
import formencode.validators
36
37
 
 
38
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
 
39
                                    DateTimeValidator)
 
40
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
37
41
from ivle.webapp.base.xhtml import XHTMLView
38
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
42
from ivle.webapp.errors import BadRequest
39
43
from ivle.webapp import ApplicationRoot
40
44
 
41
45
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
43
47
from ivle import util
44
48
import ivle.date
45
49
 
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,
 
50
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
50
51
            subject_to_offering, offering_to_projectset, offering_to_project,
51
 
            subject_url, offering_url, projectset_url, project_url)
 
52
            offering_to_enrolment, subject_url, semester_url, offering_url,
 
53
            projectset_url, project_url, enrolment_url)
52
54
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
53
 
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
55
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
 
56
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
 
57
from ivle.webapp.core import Plugin as CorePlugin
 
58
from ivle.webapp.groups import GroupsView
 
59
from ivle.webapp.media import media_url
 
60
from ivle.webapp.tutorial import Plugin as TutorialPlugin
54
61
 
55
62
class SubjectsView(XHTMLView):
56
63
    '''The view of the list of subjects.'''
57
64
    template = 'templates/subjects.html'
58
65
    tab = 'subjects'
 
66
    breadcrumb_text = "Subjects"
59
67
 
60
68
    def authorize(self, req):
61
69
        return req.user is not None
62
70
 
63
71
    def populate(self, req, ctx):
 
72
        ctx['req'] = req
64
73
        ctx['user'] = req.user
65
74
        ctx['semesters'] = []
 
75
 
66
76
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
67
77
                                                     Desc(Semester.semester)):
68
 
            enrolments = semester.enrolments.find(user=req.user)
69
 
            if enrolments.count():
70
 
                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['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
71
533
 
72
534
 
73
535
class UserValidator(formencode.FancyValidator):
94
556
        return value
95
557
 
96
558
 
 
559
class RoleEnrolmentValidator(formencode.FancyValidator):
 
560
    """A FormEncode validator that checks permission to enrol users with a
 
561
    particular role.
 
562
 
 
563
    The state must have an 'offering' attribute.
 
564
    """
 
565
    def _to_python(self, value, state):
 
566
        if (("enrol_" + value) not in
 
567
                state.offering.get_permissions(state.user, state.config)):
 
568
            raise formencode.Invalid('Not allowed to assign users that role',
 
569
                                     value, state)
 
570
        return value
 
571
 
 
572
 
97
573
class EnrolSchema(formencode.Schema):
98
574
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
575
    role = formencode.All(formencode.validators.OneOf(
 
576
                                ["lecturer", "tutor", "student"]),
 
577
                          RoleEnrolmentValidator(),
 
578
                          formencode.validators.UnicodeString())
 
579
 
 
580
 
 
581
class EnrolmentsView(XHTMLView):
 
582
    """A page which displays all users enrolled in an offering."""
 
583
    template = 'templates/enrolments.html'
 
584
    tab = 'subjects'
 
585
    permission = 'edit'
 
586
    breadcrumb_text = 'Enrolments'
 
587
 
 
588
    def populate(self, req, ctx):
 
589
        ctx['req'] = req
 
590
        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
99
597
 
100
598
 
101
599
class EnrolView(XHTMLView):
102
600
    """A form to enrol a user in an offering."""
103
601
    template = 'templates/enrol.html'
104
602
    tab = 'subjects'
105
 
    permission = 'edit'
 
603
    permission = 'enrol'
106
604
 
107
605
    def filter(self, stream, ctx):
108
606
        return stream | HTMLFormFiller(data=ctx['data'])
114
612
                validator = EnrolSchema()
115
613
                req.offering = self.context # XXX: Getting into state.
116
614
                data = validator.to_python(data, state=req)
117
 
                self.context.enrol(data['user'])
 
615
                self.context.enrol(data['user'], data['role'])
118
616
                req.store.commit()
119
617
                req.throw_redirect(req.uri)
120
618
            except formencode.Invalid, e:
125
623
 
126
624
        ctx['data'] = data or {}
127
625
        ctx['offering'] = self.context
 
626
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
128
627
        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
 
129
685
 
130
686
class OfferingProjectsView(XHTMLView):
131
687
    """View the projects for an offering."""
132
688
    template = 'templates/offering_projects.html'
133
689
    permission = 'edit'
134
690
    tab = 'subjects'
135
 
 
136
 
    def project_url(self, projectset, project):
137
 
        return "/subjects/%s/%s/%s/+projects/%s" % (
138
 
                    self.context.subject.short_name,
139
 
                    self.context.semester.year,
140
 
                    self.context.semester.semester,
141
 
                    project.short_name
142
 
                    )
143
 
 
144
 
    def new_project_url(self, projectset):
145
 
        return "/api/subjects/" + self.context.subject.short_name + "/" +\
146
 
                self.context.semester.year + "/" + \
147
 
                self.context.semester.semester + "/+projectsets/" +\
148
 
                str(projectset.id) + "/+projects/+new"
149
 
    
 
691
    breadcrumb_text = 'Projects'
 
692
 
150
693
    def populate(self, req, ctx):
151
694
        self.plugin_styles[Plugin] = ["project.css"]
152
 
        self.plugin_scripts[Plugin] = ["project.js"]
 
695
        ctx['req'] = req
153
696
        ctx['offering'] = self.context
154
697
        ctx['projectsets'] = []
155
698
 
156
699
        #Open the projectset Fragment, and render it for inclusion
157
700
        #into the ProjectSets page
158
 
        #XXX: This could be a lot cleaner
159
 
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
160
 
 
161
701
        set_fragment = os.path.join(os.path.dirname(__file__),
162
702
                "templates/projectset_fragment.html")
163
703
        project_fragment = os.path.join(os.path.dirname(__file__),
164
704
                "templates/project_fragment.html")
165
705
 
166
 
        for projectset in self.context.project_sets:
167
 
            settmpl = loader.load(set_fragment)
 
706
        for projectset in \
 
707
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
 
708
            settmpl = self._loader.load(set_fragment)
168
709
            setCtx = Context()
 
710
            setCtx['req'] = req
169
711
            setCtx['projectset'] = projectset
170
 
            setCtx['new_project_url'] = self.new_project_url(projectset)
171
712
            setCtx['projects'] = []
 
713
            setCtx['GroupsView'] = GroupsView
 
714
            setCtx['ProjectSetEdit'] = ProjectSetEdit
 
715
            setCtx['ProjectNew'] = ProjectNew
172
716
 
173
 
            for project in projectset.projects:
174
 
                projecttmpl = loader.load(project_fragment)
 
717
            for project in \
 
718
                projectset.projects.order_by(ivle.database.Project.deadline):
 
719
                projecttmpl = self._loader.load(project_fragment)
175
720
                projectCtx = Context()
 
721
                projectCtx['req'] = req
176
722
                projectCtx['project'] = project
177
 
                projectCtx['project_url'] = self.project_url(projectset, project)
 
723
                projectCtx['ProjectEdit'] = ProjectEdit
 
724
                projectCtx['ProjectDelete'] = ProjectDelete
178
725
 
179
726
                setCtx['projects'].append(
180
727
                        projecttmpl.generate(projectCtx))
185
732
class ProjectView(XHTMLView):
186
733
    """View the submissions for a ProjectSet"""
187
734
    template = "templates/project.html"
188
 
    permission = "edit"
 
735
    permission = "view_project_submissions"
189
736
    tab = 'subjects'
190
737
 
191
 
    def build_subversion_url(self, svnroot, submission):
 
738
    def build_subversion_url(self, req, submission):
192
739
        princ = submission.assessed.principal
193
740
 
194
 
        if isinstance(princ, User):
195
 
            path = 'users/%s' % princ.login
196
 
        else:
197
 
            path = 'groups/%s_%s_%s_%s' % (
198
 
                    princ.project_set.offering.subject.short_name,
199
 
                    princ.project_set.offering.semester.year,
200
 
                    princ.project_set.offering.semester.semester,
201
 
                    princ.name
202
 
                    )
203
 
        return urlparse.urljoin(
204
 
                    svnroot,
205
 
                    os.path.join(path, submission.path[1:] if
206
 
                                       submission.path.startswith(os.sep) else
207
 
                                       submission.path))
 
741
        return os.path.join(princ.get_svn_url(req.config, req),
 
742
                            submission.path[1:] if
 
743
                                submission.path.startswith(os.sep) else
 
744
                                submission.path)
208
745
 
209
746
    def populate(self, req, ctx):
210
747
        self.plugin_styles[Plugin] = ["project.css"]
211
748
 
 
749
        ctx['req'] = req
 
750
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
751
        ctx['GroupsView'] = GroupsView
 
752
        ctx['EnrolView'] = EnrolView
 
753
        ctx['format_datetime'] = ivle.date.make_date_nice
212
754
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
213
755
        ctx['build_subversion_url'] = self.build_subversion_url
214
 
        ctx['svn_addr'] = req.config['urls']['svn_addr']
215
756
        ctx['project'] = self.context
216
757
        ctx['user'] = req.user
217
 
 
218
 
class OfferingEnrolmentSet(object):
219
 
    def __init__(self, offering):
220
 
        self.offering = offering
 
758
        ctx['ProjectEdit'] = ProjectEdit
 
759
        ctx['ProjectDelete'] = ProjectDelete
 
760
 
 
761
class ProjectUniquenessValidator(formencode.FancyValidator):
 
762
    """A FormEncode validator that checks that a project short_name is unique
 
763
    in a given offering.
 
764
 
 
765
    The project referenced by state.existing_project is permitted to
 
766
    hold that short_name. If any other project holds it, the input is rejected.
 
767
    """
 
768
    def _to_python(self, value, state):
 
769
        if (state.store.find(
 
770
            Project,
 
771
            Project.short_name == unicode(value),
 
772
            Project.project_set_id == ProjectSet.id,
 
773
            ProjectSet.offering == state.offering).one() not in
 
774
            (None, state.existing_project)):
 
775
            raise formencode.Invalid(
 
776
                "A project with that URL name already exists in this offering."
 
777
                , value, state)
 
778
        return value
 
779
 
 
780
class ProjectSchema(formencode.Schema):
 
781
    name = formencode.validators.UnicodeString(not_empty=True)
 
782
    short_name = formencode.All(
 
783
        URLNameValidator(not_empty=True),
 
784
        ProjectUniquenessValidator())
 
785
    deadline = DateTimeValidator(not_empty=True)
 
786
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
787
    synopsis = formencode.validators.UnicodeString(not_empty=True)
 
788
 
 
789
class ProjectEdit(BaseFormView):
 
790
    """A form to edit a project."""
 
791
    template = 'templates/project-edit.html'
 
792
    tab = 'subjects'
 
793
    permission = 'edit'
 
794
 
 
795
    @property
 
796
    def validator(self):
 
797
        return ProjectSchema()
 
798
 
 
799
    def populate(self, req, ctx):
 
800
        super(ProjectEdit, self).populate(req, ctx)
 
801
        ctx['projectset'] = self.context.project_set
 
802
 
 
803
    def populate_state(self, state):
 
804
        state.offering = self.context.project_set.offering
 
805
        state.existing_project = self.context
 
806
 
 
807
    def get_default_data(self, req):
 
808
        return {
 
809
            'name':         self.context.name,
 
810
            'short_name':   self.context.short_name,
 
811
            'deadline':     self.context.deadline,
 
812
            'url':          self.context.url,
 
813
            'synopsis':     self.context.synopsis,
 
814
            }
 
815
 
 
816
    def save_object(self, req, data):
 
817
        self.context.name = data['name']
 
818
        self.context.short_name = data['short_name']
 
819
        self.context.deadline = data['deadline']
 
820
        self.context.url = unicode(data['url']) if data['url'] else None
 
821
        self.context.synopsis = data['synopsis']
 
822
        return self.context
 
823
 
 
824
class ProjectNew(BaseFormView):
 
825
    """A form to create a new project."""
 
826
    template = 'templates/project-new.html'
 
827
    tab = 'subjects'
 
828
    permission = 'edit'
 
829
 
 
830
    @property
 
831
    def validator(self):
 
832
        return ProjectSchema()
 
833
 
 
834
    def populate(self, req, ctx):
 
835
        super(ProjectNew, self).populate(req, ctx)
 
836
        ctx['projectset'] = self.context
 
837
 
 
838
    def populate_state(self, state):
 
839
        state.offering = self.context.offering
 
840
        state.existing_project = None
 
841
 
 
842
    def get_default_data(self, req):
 
843
        return {}
 
844
 
 
845
    def save_object(self, req, data):
 
846
        new_project = Project()
 
847
        new_project.project_set = self.context
 
848
        new_project.name = data['name']
 
849
        new_project.short_name = data['short_name']
 
850
        new_project.deadline = data['deadline']
 
851
        new_project.url = unicode(data['url']) if data['url'] else None
 
852
        new_project.synopsis = data['synopsis']
 
853
        req.store.add(new_project)
 
854
        return new_project
 
855
 
 
856
class ProjectDelete(XHTMLView):
 
857
    """A form to delete a project."""
 
858
    template = 'templates/project-delete.html'
 
859
    tab = 'subjects'
 
860
    permission = 'edit'
 
861
 
 
862
    def populate(self, req, ctx):
 
863
        # If post, delete the project, or display a message explaining that
 
864
        # the project cannot be deleted
 
865
        if self.context.can_delete:
 
866
            if req.method == 'POST':
 
867
                self.context.delete()
 
868
                self.template = 'templates/project-deleted.html'
 
869
        else:
 
870
            # Can't delete
 
871
            self.template = 'templates/project-undeletable.html'
 
872
 
 
873
        # If get and can delete, display a delete confirmation page
 
874
 
 
875
        # Variables for the template
 
876
        ctx['req'] = req
 
877
        ctx['project'] = self.context
 
878
        ctx['OfferingProjectsView'] = OfferingProjectsView
 
879
 
 
880
class ProjectSetSchema(formencode.Schema):
 
881
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
 
882
 
 
883
class ProjectSetEdit(BaseFormView):
 
884
    """A form to edit a project set."""
 
885
    template = 'templates/projectset-edit.html'
 
886
    tab = 'subjects'
 
887
    permission = 'edit'
 
888
 
 
889
    @property
 
890
    def validator(self):
 
891
        return ProjectSetSchema()
 
892
 
 
893
    def populate(self, req, ctx):
 
894
        super(ProjectSetEdit, self).populate(req, ctx)
 
895
 
 
896
    def get_default_data(self, req):
 
897
        return {
 
898
            'group_size': self.context.max_students_per_group,
 
899
            }
 
900
 
 
901
    def save_object(self, req, data):
 
902
        self.context.max_students_per_group = data['group_size']
 
903
        return self.context
 
904
 
 
905
class ProjectSetNew(BaseFormView):
 
906
    """A form to create a new project set."""
 
907
    template = 'templates/projectset-new.html'
 
908
    tab = 'subjects'
 
909
    permission = 'edit'
 
910
    breadcrumb_text = "Projects"
 
911
 
 
912
    @property
 
913
    def validator(self):
 
914
        return ProjectSetSchema()
 
915
 
 
916
    def populate(self, req, ctx):
 
917
        super(ProjectSetNew, self).populate(req, ctx)
 
918
 
 
919
    def get_default_data(self, req):
 
920
        return {}
 
921
 
 
922
    def save_object(self, req, data):
 
923
        new_set = ProjectSet()
 
924
        new_set.offering = self.context
 
925
        new_set.max_students_per_group = data['group_size']
 
926
        req.store.add(new_set)
 
927
        return new_set
221
928
 
222
929
class Plugin(ViewPlugin, MediaPlugin):
223
 
    forward_routes = (root_to_subject, subject_to_offering,
224
 
                      offering_to_project, offering_to_projectset)
225
 
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
930
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
 
931
                      offering_to_project, offering_to_projectset,
 
932
                      offering_to_enrolment)
 
933
    reverse_routes = (
 
934
        subject_url, semester_url, offering_url, projectset_url, project_url,
 
935
        enrolment_url)
226
936
 
227
937
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
938
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
 
939
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
940
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
 
941
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
 
942
             (Subject, '+index', SubjectView),
 
943
             (Subject, '+edit', SubjectEdit),
 
944
             (Subject, '+new-offering', SubjectOfferingNew),
 
945
             (Semester, '+edit', SemesterEdit),
 
946
             (Offering, '+index', OfferingView),
 
947
             (Offering, '+edit', OfferingEdit),
 
948
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
 
949
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
228
950
             (Offering, ('+enrolments', '+new'), EnrolView),
 
951
             (Enrolment, '+edit', EnrolmentEdit),
 
952
             (Enrolment, '+delete', EnrolmentDelete),
229
953
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
954
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
 
955
             (ProjectSet, '+edit', ProjectSetEdit),
 
956
             (ProjectSet, '+new', ProjectNew),
230
957
             (Project, '+index', ProjectView),
231
 
 
232
 
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
233
 
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
234
 
             (Project, '+index', ProjectRESTView, 'api'),
 
958
             (Project, '+edit', ProjectEdit),
 
959
             (Project, '+delete', ProjectDelete),
235
960
             ]
236
961
 
237
962
    breadcrumbs = {Subject: SubjectBreadcrumb,
238
963
                   Offering: OfferingBreadcrumb,
239
964
                   User: UserBreadcrumb,
240
965
                   Project: ProjectBreadcrumb,
 
966
                   Enrolment: EnrolmentBreadcrumb,
241
967
                   }
242
968
 
243
969
    tabs = [