~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: 2012-06-28 01:52:02 UTC
  • Revision ID: me@williamgrant.id.au-20120628015202-f6ru7o367gt6nvgz
Hah

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