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

« back to all changes in this revision

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

Fixed a broken link when adding a new project.

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