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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2010-02-16 07:22:43 UTC
  • mto: This revision was merged to the branch mainline in revision 1674.
  • Revision ID: grantw@unimelb.edu.au-20100216072243-iq9ej3kz3xq6t0ja
Fail nicely if a project with the same name already exists.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
17
 
 
18
# App: subjects
 
19
# Author: Matt Giuca
 
20
# Date: 29/2/2008
 
21
 
 
22
# This is an IVLE application.
 
23
# A sample / testing application for IVLE.
 
24
 
 
25
import os
 
26
import os.path
 
27
import urllib
 
28
import urlparse
 
29
import cgi
 
30
 
 
31
from storm.locals import Desc, Store
 
32
import genshi
 
33
from genshi.filters import HTMLFormFiller
 
34
from genshi.template import Context, TemplateLoader
 
35
import formencode
 
36
import formencode.validators
 
37
 
 
38
from ivle.webapp.base.forms import BaseFormView, URLNameValidator
 
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
40
from ivle.webapp.base.xhtml import XHTMLView
 
41
from ivle.webapp.errors import BadRequest
 
42
from ivle.webapp import ApplicationRoot
 
43
 
 
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
45
                          ProjectSet, Project, ProjectSubmission
 
46
from ivle import util
 
47
import ivle.date
 
48
 
 
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
 
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
 
52
            subject_to_offering, offering_to_projectset, offering_to_project,
 
53
            subject_url, semester_url, offering_url, projectset_url,
 
54
            project_url)
 
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
 
56
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
57
from ivle.webapp.core import Plugin as CorePlugin
 
58
from ivle.webapp.groups import GroupsView
 
59
from ivle.webapp.media import media_url
 
60
from ivle.webapp.tutorial import Plugin as TutorialPlugin
 
61
 
 
62
class SubjectsView(XHTMLView):
 
63
    '''The view of the list of subjects.'''
 
64
    template = 'templates/subjects.html'
 
65
    tab = 'subjects'
 
66
 
 
67
    def authorize(self, req):
 
68
        return req.user is not None
 
69
 
 
70
    def populate(self, req, ctx):
 
71
        ctx['req'] = req
 
72
        ctx['user'] = req.user
 
73
        ctx['semesters'] = []
 
74
 
 
75
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
 
76
                                                     Desc(Semester.semester)):
 
77
            if req.user.admin:
 
78
                # For admins, show all subjects in the system
 
79
                offerings = list(semester.offerings.find())
 
80
            else:
 
81
                offerings = [enrolment.offering for enrolment in
 
82
                                    semester.enrolments.find(user=req.user)]
 
83
            if len(offerings):
 
84
                ctx['semesters'].append((semester, offerings))
 
85
 
 
86
 
 
87
class SubjectsManage(XHTMLView):
 
88
    '''Subject management view.'''
 
89
    template = 'templates/subjects-manage.html'
 
90
    tab = 'subjects'
 
91
 
 
92
    def authorize(self, req):
 
93
        return req.user is not None and req.user.admin
 
94
 
 
95
    def populate(self, req, ctx):
 
96
        ctx['req'] = req
 
97
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
98
        ctx['SubjectEdit'] = SubjectEdit
 
99
        ctx['SemesterEdit'] = SemesterEdit
 
100
 
 
101
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
102
        ctx['semesters'] = req.store.find(Semester).order_by(
 
103
            Semester.year, Semester.semester)
 
104
 
 
105
 
 
106
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
107
    """A FormEncode validator that checks that a subject name is unused.
 
108
 
 
109
    The subject referenced by state.existing_subject is permitted
 
110
    to hold that name. If any other object holds it, the input is rejected.
 
111
    """
 
112
    def __init__(self, matching=None):
 
113
        self.matching = matching
 
114
 
 
115
    def _to_python(self, value, state):
 
116
        if (state.store.find(
 
117
                Subject, short_name=value).one() not in
 
118
                (None, state.existing_subject)):
 
119
            raise formencode.Invalid(
 
120
                'Short name already taken', value, state)
 
121
        return value
 
122
 
 
123
 
 
124
class SubjectSchema(formencode.Schema):
 
125
    short_name = formencode.All(
 
126
        SubjectShortNameUniquenessValidator(),
 
127
        URLNameValidator(not_empty=True))
 
128
    name = formencode.validators.UnicodeString(not_empty=True)
 
129
    code = formencode.validators.UnicodeString(not_empty=True)
 
130
 
 
131
 
 
132
class SubjectFormView(BaseFormView):
 
133
    """An abstract form to add or edit a subject."""
 
134
    tab = 'subjects'
 
135
 
 
136
    def authorize(self, req):
 
137
        return req.user is not None and req.user.admin
 
138
 
 
139
    def populate_state(self, state):
 
140
        state.existing_subject = None
 
141
 
 
142
    @property
 
143
    def validator(self):
 
144
        return SubjectSchema()
 
145
 
 
146
    def get_return_url(self, obj):
 
147
        return '/subjects'
 
148
 
 
149
 
 
150
class SubjectNew(SubjectFormView):
 
151
    """A form to create a subject."""
 
152
    template = 'templates/subject-new.html'
 
153
 
 
154
    def get_default_data(self, req):
 
155
        return {}
 
156
 
 
157
    def save_object(self, req, data):
 
158
        new_subject = Subject()
 
159
        new_subject.short_name = data['short_name']
 
160
        new_subject.name = data['name']
 
161
        new_subject.code = data['code']
 
162
 
 
163
        req.store.add(new_subject)
 
164
        return new_subject
 
165
 
 
166
 
 
167
class SubjectEdit(SubjectFormView):
 
168
    """A form to edit a subject."""
 
169
    template = 'templates/subject-edit.html'
 
170
 
 
171
    def populate_state(self, state):
 
172
        state.existing_subject = self.context
 
173
 
 
174
    def get_default_data(self, req):
 
175
        return {
 
176
            'short_name': self.context.short_name,
 
177
            'name': self.context.name,
 
178
            'code': self.context.code,
 
179
            }
 
180
 
 
181
    def save_object(self, req, data):
 
182
        self.context.short_name = data['short_name']
 
183
        self.context.name = data['name']
 
184
        self.context.code = data['code']
 
185
 
 
186
        return self.context
 
187
 
 
188
 
 
189
class SemesterUniquenessValidator(formencode.FancyValidator):
 
190
    """A FormEncode validator that checks that a semester is unique.
 
191
 
 
192
    There cannot be more than one semester for the same year and semester.
 
193
    """
 
194
    def _to_python(self, value, state):
 
195
        if (state.store.find(
 
196
                Semester, year=value['year'], semester=value['semester']
 
197
                ).one() not in (None, state.existing_semester)):
 
198
            raise formencode.Invalid(
 
199
                'Semester already exists', value, state)
 
200
        return value
 
201
 
 
202
 
 
203
class SemesterSchema(formencode.Schema):
 
204
    year = URLNameValidator()
 
205
    semester = URLNameValidator()
 
206
    state = formencode.All(
 
207
        formencode.validators.OneOf(["past", "current", "future"]),
 
208
        formencode.validators.UnicodeString())
 
209
    chained_validators = [SemesterUniquenessValidator()]
 
210
 
 
211
 
 
212
class SemesterFormView(BaseFormView):
 
213
    tab = 'subjects'
 
214
 
 
215
    def authorize(self, req):
 
216
        return req.user is not None and req.user.admin
 
217
 
 
218
    @property
 
219
    def validator(self):
 
220
        return SemesterSchema()
 
221
 
 
222
    def get_return_url(self, obj):
 
223
        return '/subjects/+manage'
 
224
 
 
225
 
 
226
class SemesterNew(SemesterFormView):
 
227
    """A form to create a semester."""
 
228
    template = 'templates/semester-new.html'
 
229
    tab = 'subjects'
 
230
 
 
231
    def populate_state(self, state):
 
232
        state.existing_semester = None
 
233
 
 
234
    def get_default_data(self, req):
 
235
        return {}
 
236
 
 
237
    def save_object(self, req, data):
 
238
        new_semester = Semester()
 
239
        new_semester.year = data['year']
 
240
        new_semester.semester = data['semester']
 
241
        new_semester.state = data['state']
 
242
 
 
243
        req.store.add(new_semester)
 
244
        return new_semester
 
245
 
 
246
 
 
247
class SemesterEdit(SemesterFormView):
 
248
    """A form to edit a semester."""
 
249
    template = 'templates/semester-edit.html'
 
250
 
 
251
    def populate_state(self, state):
 
252
        state.existing_semester = self.context
 
253
 
 
254
    def get_default_data(self, req):
 
255
        return {
 
256
            'year': self.context.year,
 
257
            'semester': self.context.semester,
 
258
            'state': self.context.state,
 
259
            }
 
260
 
 
261
    def save_object(self, req, data):
 
262
        self.context.year = data['year']
 
263
        self.context.semester = data['semester']
 
264
        self.context.state = data['state']
 
265
 
 
266
        return self.context
 
267
 
 
268
 
 
269
class OfferingView(XHTMLView):
 
270
    """The home page of an offering."""
 
271
    template = 'templates/offering.html'
 
272
    tab = 'subjects'
 
273
    permission = 'view'
 
274
 
 
275
    def populate(self, req, ctx):
 
276
        # Need the worksheet result styles.
 
277
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
278
        ctx['context'] = self.context
 
279
        ctx['req'] = req
 
280
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
 
281
        ctx['format_submission_principal'] = util.format_submission_principal
 
282
        ctx['format_datetime'] = ivle.date.make_date_nice
 
283
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
284
        ctx['OfferingEdit'] = OfferingEdit
 
285
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
 
286
        ctx['GroupsView'] = GroupsView
 
287
 
 
288
        # As we go, calculate the total score for this subject
 
289
        # (Assessable worksheets only, mandatory problems only)
 
290
 
 
291
        ctx['worksheets'], problems_total, problems_done = (
 
292
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
293
                req.store, req.user, self.context))
 
294
 
 
295
        ctx['exercises_total'] = problems_total
 
296
        ctx['exercises_done'] = problems_done
 
297
        if problems_total > 0:
 
298
            if problems_done >= problems_total:
 
299
                ctx['worksheets_complete_class'] = "complete"
 
300
            elif problems_done > 0:
 
301
                ctx['worksheets_complete_class'] = "semicomplete"
 
302
            else:
 
303
                ctx['worksheets_complete_class'] = "incomplete"
 
304
            # Calculate the final percentage and mark for the subject
 
305
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
306
             ctx['worksheet_max_mark']) = (
 
307
                ivle.worksheet.utils.calculate_mark(
 
308
                    problems_done, problems_total))
 
309
 
 
310
 
 
311
class SubjectValidator(formencode.FancyValidator):
 
312
    """A FormEncode validator that turns a subject name into a subject.
 
313
 
 
314
    The state must have a 'store' attribute, which is the Storm store
 
315
    to use.
 
316
    """
 
317
    def _to_python(self, value, state):
 
318
        subject = state.store.find(Subject, short_name=value).one()
 
319
        if subject:
 
320
            return subject
 
321
        else:
 
322
            raise formencode.Invalid('Subject does not exist', value, state)
 
323
 
 
324
 
 
325
class SemesterValidator(formencode.FancyValidator):
 
326
    """A FormEncode validator that turns a string into a semester.
 
327
 
 
328
    The string should be of the form 'year/semester', eg. '2009/1'.
 
329
 
 
330
    The state must have a 'store' attribute, which is the Storm store
 
331
    to use.
 
332
    """
 
333
    def _to_python(self, value, state):
 
334
        try:
 
335
            year, semester = value.split('/')
 
336
        except ValueError:
 
337
            year = semester = None
 
338
 
 
339
        semester = state.store.find(
 
340
            Semester, year=year, semester=semester).one()
 
341
        if semester:
 
342
            return semester
 
343
        else:
 
344
            raise formencode.Invalid('Semester does not exist', value, state)
 
345
 
 
346
 
 
347
class OfferingUniquenessValidator(formencode.FancyValidator):
 
348
    """A FormEncode validator that checks that an offering is unique.
 
349
 
 
350
    There cannot be more than one offering in the same year and semester.
 
351
 
 
352
    The offering referenced by state.existing_offering is permitted to
 
353
    hold that year and semester tuple. If any other object holds it, the
 
354
    input is rejected.
 
355
    """
 
356
    def _to_python(self, value, state):
 
357
        if (state.store.find(
 
358
                Offering, subject=value['subject'],
 
359
                semester=value['semester']).one() not in
 
360
                (None, state.existing_offering)):
 
361
            raise formencode.Invalid(
 
362
                'Offering already exists', value, state)
 
363
        return value
 
364
 
 
365
 
 
366
class OfferingSchema(formencode.Schema):
 
367
    description = formencode.validators.UnicodeString(
 
368
        if_missing=None, not_empty=False)
 
369
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
370
 
 
371
 
 
372
class OfferingAdminSchema(OfferingSchema):
 
373
    subject = formencode.All(
 
374
        SubjectValidator(), formencode.validators.UnicodeString())
 
375
    semester = formencode.All(
 
376
        SemesterValidator(), formencode.validators.UnicodeString())
 
377
    chained_validators = [OfferingUniquenessValidator()]
 
378
 
 
379
 
 
380
class OfferingEdit(BaseFormView):
 
381
    """A form to edit an offering's details."""
 
382
    template = 'templates/offering-edit.html'
 
383
    tab = 'subjects'
 
384
    permission = 'edit'
 
385
 
 
386
    @property
 
387
    def validator(self):
 
388
        if self.req.user.admin:
 
389
            return OfferingAdminSchema()
 
390
        else:
 
391
            return OfferingSchema()
 
392
 
 
393
    def populate(self, req, ctx):
 
394
        super(OfferingEdit, self).populate(req, ctx)
 
395
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
396
        ctx['semesters'] = req.store.find(Semester).order_by(
 
397
            Semester.year, Semester.semester)
 
398
 
 
399
    def populate_state(self, state):
 
400
        state.existing_offering = self.context
 
401
 
 
402
    def get_default_data(self, req):
 
403
        return {
 
404
            'subject': self.context.subject.short_name,
 
405
            'semester': self.context.semester.year + '/' +
 
406
                        self.context.semester.semester,
 
407
            'url': self.context.url,
 
408
            'description': self.context.description,
 
409
            }
 
410
 
 
411
    def save_object(self, req, data):
 
412
        if req.user.admin:
 
413
            self.context.subject = data['subject']
 
414
            self.context.semester = data['semester']
 
415
        self.context.description = data['description']
 
416
        self.context.url = unicode(data['url']) if data['url'] else None
 
417
        return self.context
 
418
 
 
419
 
 
420
class OfferingNew(BaseFormView):
 
421
    """A form to create an offering."""
 
422
    template = 'templates/offering-new.html'
 
423
    tab = 'subjects'
 
424
 
 
425
    def authorize(self, req):
 
426
        return req.user is not None and req.user.admin
 
427
 
 
428
    @property
 
429
    def validator(self):
 
430
        return OfferingAdminSchema()
 
431
 
 
432
    def populate(self, req, ctx):
 
433
        super(OfferingNew, self).populate(req, ctx)
 
434
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
435
        ctx['semesters'] = req.store.find(Semester).order_by(
 
436
            Semester.year, Semester.semester)
 
437
 
 
438
    def populate_state(self, state):
 
439
        state.existing_offering = None
 
440
 
 
441
    def get_default_data(self, req):
 
442
        return {}
 
443
 
 
444
    def save_object(self, req, data):
 
445
        new_offering = Offering()
 
446
        new_offering.subject = data['subject']
 
447
        new_offering.semester = data['semester']
 
448
        new_offering.description = data['description']
 
449
        new_offering.url = unicode(data['url']) if data['url'] else None
 
450
 
 
451
        req.store.add(new_offering)
 
452
        return new_offering
 
453
 
 
454
 
 
455
class OfferingCloneWorksheetsSchema(formencode.Schema):
 
456
    subject = formencode.All(
 
457
        SubjectValidator(), formencode.validators.UnicodeString())
 
458
    semester = formencode.All(
 
459
        SemesterValidator(), formencode.validators.UnicodeString())
 
460
 
 
461
 
 
462
class OfferingCloneWorksheets(BaseFormView):
 
463
    """A form to clone worksheets from one offering to another."""
 
464
    template = 'templates/offering-clone-worksheets.html'
 
465
    tab = 'subjects'
 
466
 
 
467
    def authorize(self, req):
 
468
        return req.user is not None and req.user.admin
 
469
 
 
470
    @property
 
471
    def validator(self):
 
472
        return OfferingCloneWorksheetsSchema()
 
473
 
 
474
    def populate(self, req, ctx):
 
475
        super(OfferingCloneWorksheets, self).populate(req, ctx)
 
476
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
477
        ctx['semesters'] = req.store.find(Semester).order_by(
 
478
            Semester.year, Semester.semester)
 
479
 
 
480
    def get_default_data(self, req):
 
481
        return {}
 
482
 
 
483
    def save_object(self, req, data):
 
484
        if self.context.worksheets.count() > 0:
 
485
            raise BadRequest(
 
486
                "Cannot clone to target with existing worksheets.")
 
487
        offering = req.store.find(
 
488
            Offering, subject=data['subject'], semester=data['semester']).one()
 
489
        if offering is None:
 
490
            raise BadRequest("No such offering.")
 
491
        if offering.worksheets.count() == 0:
 
492
            raise BadRequest("Source offering has no worksheets.")
 
493
 
 
494
        self.context.clone_worksheets(offering)
 
495
        return self.context
 
496
 
 
497
 
 
498
class UserValidator(formencode.FancyValidator):
 
499
    """A FormEncode validator that turns a username into a user.
 
500
 
 
501
    The state must have a 'store' attribute, which is the Storm store
 
502
    to use."""
 
503
    def _to_python(self, value, state):
 
504
        user = User.get_by_login(state.store, value)
 
505
        if user:
 
506
            return user
 
507
        else:
 
508
            raise formencode.Invalid('User does not exist', value, state)
 
509
 
 
510
 
 
511
class NoEnrolmentValidator(formencode.FancyValidator):
 
512
    """A FormEncode validator that ensures absence of an enrolment.
 
513
 
 
514
    The state must have an 'offering' attribute.
 
515
    """
 
516
    def _to_python(self, value, state):
 
517
        if state.offering.get_enrolment(value):
 
518
            raise formencode.Invalid('User already enrolled', value, state)
 
519
        return value
 
520
 
 
521
 
 
522
class RoleEnrolmentValidator(formencode.FancyValidator):
 
523
    """A FormEncode validator that checks permission to enrol users with a
 
524
    particular role.
 
525
 
 
526
    The state must have an 'offering' attribute.
 
527
    """
 
528
    def _to_python(self, value, state):
 
529
        if (("enrol_" + value) not in
 
530
                state.offering.get_permissions(state.user, state.config)):
 
531
            raise formencode.Invalid('Not allowed to assign users that role',
 
532
                                     value, state)
 
533
        return value
 
534
 
 
535
 
 
536
class EnrolSchema(formencode.Schema):
 
537
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
538
    role = formencode.All(formencode.validators.OneOf(
 
539
                                ["lecturer", "tutor", "student"]),
 
540
                          RoleEnrolmentValidator(),
 
541
                          formencode.validators.UnicodeString())
 
542
 
 
543
 
 
544
class EnrolmentsView(XHTMLView):
 
545
    """A page which displays all users enrolled in an offering."""
 
546
    template = 'templates/enrolments.html'
 
547
    tab = 'subjects'
 
548
    permission = 'edit'
 
549
 
 
550
    def populate(self, req, ctx):
 
551
        ctx['offering'] = self.context
 
552
 
 
553
class EnrolView(XHTMLView):
 
554
    """A form to enrol a user in an offering."""
 
555
    template = 'templates/enrol.html'
 
556
    tab = 'subjects'
 
557
    permission = 'enrol'
 
558
 
 
559
    def filter(self, stream, ctx):
 
560
        return stream | HTMLFormFiller(data=ctx['data'])
 
561
 
 
562
    def populate(self, req, ctx):
 
563
        if req.method == 'POST':
 
564
            data = dict(req.get_fieldstorage())
 
565
            try:
 
566
                validator = EnrolSchema()
 
567
                req.offering = self.context # XXX: Getting into state.
 
568
                data = validator.to_python(data, state=req)
 
569
                self.context.enrol(data['user'], data['role'])
 
570
                req.store.commit()
 
571
                req.throw_redirect(req.uri)
 
572
            except formencode.Invalid, e:
 
573
                errors = e.unpack_errors()
 
574
        else:
 
575
            data = {}
 
576
            errors = {}
 
577
 
 
578
        ctx['data'] = data or {}
 
579
        ctx['offering'] = self.context
 
580
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
 
581
        ctx['errors'] = errors
 
582
 
 
583
class OfferingProjectsView(XHTMLView):
 
584
    """View the projects for an offering."""
 
585
    template = 'templates/offering_projects.html'
 
586
    permission = 'edit'
 
587
    tab = 'subjects'
 
588
 
 
589
    def populate(self, req, ctx):
 
590
        self.plugin_styles[Plugin] = ["project.css"]
 
591
        self.plugin_scripts[Plugin] = ["project.js"]
 
592
        ctx['req'] = req
 
593
        ctx['offering'] = self.context
 
594
        ctx['projectsets'] = []
 
595
        ctx['OfferingRESTView'] = OfferingRESTView
 
596
 
 
597
        #Open the projectset Fragment, and render it for inclusion
 
598
        #into the ProjectSets page
 
599
        #XXX: This could be a lot cleaner
 
600
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
601
 
 
602
        set_fragment = os.path.join(os.path.dirname(__file__),
 
603
                "templates/projectset_fragment.html")
 
604
        project_fragment = os.path.join(os.path.dirname(__file__),
 
605
                "templates/project_fragment.html")
 
606
 
 
607
        for projectset in self.context.project_sets:
 
608
            settmpl = loader.load(set_fragment)
 
609
            setCtx = Context()
 
610
            setCtx['req'] = req
 
611
            setCtx['projectset'] = projectset
 
612
            setCtx['projects'] = []
 
613
            setCtx['GroupsView'] = GroupsView
 
614
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
615
 
 
616
            for project in projectset.projects:
 
617
                projecttmpl = loader.load(project_fragment)
 
618
                projectCtx = Context()
 
619
                projectCtx['req'] = req
 
620
                projectCtx['project'] = project
 
621
 
 
622
                setCtx['projects'].append(
 
623
                        projecttmpl.generate(projectCtx))
 
624
 
 
625
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
626
 
 
627
 
 
628
class ProjectView(XHTMLView):
 
629
    """View the submissions for a ProjectSet"""
 
630
    template = "templates/project.html"
 
631
    permission = "view_project_submissions"
 
632
    tab = 'subjects'
 
633
 
 
634
    def build_subversion_url(self, svnroot, submission):
 
635
        princ = submission.assessed.principal
 
636
 
 
637
        if isinstance(princ, User):
 
638
            path = 'users/%s' % princ.login
 
639
        else:
 
640
            path = 'groups/%s_%s_%s_%s' % (
 
641
                    princ.project_set.offering.subject.short_name,
 
642
                    princ.project_set.offering.semester.year,
 
643
                    princ.project_set.offering.semester.semester,
 
644
                    princ.name
 
645
                    )
 
646
        return urlparse.urljoin(
 
647
                    svnroot,
 
648
                    os.path.join(path, submission.path[1:] if
 
649
                                       submission.path.startswith(os.sep) else
 
650
                                       submission.path))
 
651
 
 
652
    def populate(self, req, ctx):
 
653
        self.plugin_styles[Plugin] = ["project.css"]
 
654
 
 
655
        ctx['req'] = req
 
656
        ctx['GroupsView'] = GroupsView
 
657
        ctx['EnrolView'] = EnrolView
 
658
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
659
        ctx['build_subversion_url'] = self.build_subversion_url
 
660
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
661
        ctx['project'] = self.context
 
662
        ctx['user'] = req.user
 
663
 
 
664
class Plugin(ViewPlugin, MediaPlugin):
 
665
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
 
666
                      offering_to_project, offering_to_projectset)
 
667
    reverse_routes = (
 
668
        subject_url, semester_url, offering_url, projectset_url, project_url)
 
669
 
 
670
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
671
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
 
672
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
673
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
 
674
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
 
675
             (Subject, '+edit', SubjectEdit),
 
676
             (Semester, '+edit', SemesterEdit),
 
677
             (Offering, '+index', OfferingView),
 
678
             (Offering, '+edit', OfferingEdit),
 
679
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
 
680
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
681
             (Offering, ('+enrolments', '+new'), EnrolView),
 
682
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
683
             (Project, '+index', ProjectView),
 
684
 
 
685
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
686
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
687
             ]
 
688
 
 
689
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
690
                   Offering: OfferingBreadcrumb,
 
691
                   User: UserBreadcrumb,
 
692
                   Project: ProjectBreadcrumb,
 
693
                   }
 
694
 
 
695
    tabs = [
 
696
        ('subjects', 'Subjects',
 
697
         'View subject content and complete worksheets',
 
698
         'subjects.png', 'subjects', 5)
 
699
    ]
 
700
 
 
701
    media = 'subject-media'