~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: 2009-06-29 01:55:55 UTC
  • mto: This revision was merged to the branch mainline in revision 1322.
  • Revision ID: grantw@unimelb.edu.au-20090629015555-49xxv0piiieunx8e
Add docs on configuring Apache.

Show diffs side-by-side

added added

removed removed

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