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

« back to all changes in this revision

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

  • Committer: mattgiuca
  • Date: 2008-01-11 04:01:16 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:176
util: Rewrote pathlist_to_path in terms of path_join (more robust and
    shorter).
        Added function ajax_call. Able to make GET and POST (urlencoded) Ajax
        requests and return a response object.

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
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
 
        formencode.validators.UnicodeString(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 = formencode.validators.UnicodeString()
205
 
    semester = formencode.validators.UnicodeString()
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'