~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 06:38:45 UTC
  • mto: This revision was merged to the branch mainline in revision 1674.
  • Revision ID: grantw@unimelb.edu.au-20100216063845-q3x2rzx3g8li0h7e
Add DB constraints for names used in URLs.

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
import urlparse
29
29
import cgi
30
30
 
31
 
from storm.locals import Desc
 
31
from storm.locals import Desc, Store
32
32
import genshi
33
33
from genshi.filters import HTMLFormFiller
34
34
from genshi.template import Context, TemplateLoader
35
35
import formencode
 
36
import formencode.validators
36
37
 
 
38
from ivle.webapp.base.forms import BaseFormView
 
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
37
40
from ivle.webapp.base.xhtml import XHTMLView
38
 
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
39
 
from ivle.webapp.errors import NotFound
 
41
from ivle.webapp.errors import BadRequest
 
42
from ivle.webapp import ApplicationRoot
40
43
 
41
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
45
                          ProjectSet, Project, ProjectSubmission
43
46
from ivle import util
44
47
import ivle.date
45
48
 
46
 
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
47
 
                                             ProjectRESTView
 
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
48
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
49
 
 
 
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
50
61
 
51
62
class SubjectsView(XHTMLView):
52
63
    '''The view of the list of subjects.'''
57
68
        return req.user is not None
58
69
 
59
70
    def populate(self, req, ctx):
 
71
        ctx['req'] = req
60
72
        ctx['user'] = req.user
61
73
        ctx['semesters'] = []
 
74
 
62
75
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
63
76
                                                     Desc(Semester.semester)):
64
 
            enrolments = semester.enrolments.find(user=req.user)
65
 
            if enrolments.count():
66
 
                ctx['semesters'].append((semester, enrolments))
 
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
67
496
 
68
497
 
69
498
class UserValidator(formencode.FancyValidator):
90
519
        return value
91
520
 
92
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
 
93
536
class EnrolSchema(formencode.Schema):
94
537
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
95
 
 
 
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
96
552
 
97
553
class EnrolView(XHTMLView):
98
554
    """A form to enrol a user in an offering."""
99
555
    template = 'templates/enrol.html'
100
556
    tab = 'subjects'
101
 
    permission = 'edit'
102
 
 
103
 
    def __init__(self, req, subject, year, semester):
104
 
        """Find the given offering by subject, year and semester."""
105
 
        self.context = req.store.find(Offering,
106
 
            Offering.subject_id == Subject.id,
107
 
            Subject.short_name == subject,
108
 
            Offering.semester_id == Semester.id,
109
 
            Semester.year == year,
110
 
            Semester.semester == semester).one()
111
 
 
112
 
        if not self.context:
113
 
            raise NotFound()
 
557
    permission = 'enrol'
114
558
 
115
559
    def filter(self, stream, ctx):
116
560
        return stream | HTMLFormFiller(data=ctx['data'])
122
566
                validator = EnrolSchema()
123
567
                req.offering = self.context # XXX: Getting into state.
124
568
                data = validator.to_python(data, state=req)
125
 
                self.context.enrol(data['user'])
 
569
                self.context.enrol(data['user'], data['role'])
126
570
                req.store.commit()
127
571
                req.throw_redirect(req.uri)
128
572
            except formencode.Invalid, e:
133
577
 
134
578
        ctx['data'] = data or {}
135
579
        ctx['offering'] = self.context
 
580
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
136
581
        ctx['errors'] = errors
137
582
 
138
583
class OfferingProjectsView(XHTMLView):
140
585
    template = 'templates/offering_projects.html'
141
586
    permission = 'edit'
142
587
    tab = 'subjects'
143
 
    
144
 
    def __init__(self, req, subject, year, semester):
145
 
        self.context = req.store.find(Offering,
146
 
            Offering.subject_id == Subject.id,
147
 
            Subject.short_name == subject,
148
 
            Offering.semester_id == Semester.id,
149
 
            Semester.year == year,
150
 
            Semester.semester == semester).one()
151
 
 
152
 
        if not self.context:
153
 
            raise NotFound()
154
 
 
155
 
    def project_url(self, projectset, project):
156
 
        return "/subjects/%s/%s/%s/+projects/%s" % (
157
 
                    self.context.subject.short_name,
158
 
                    self.context.semester.year,
159
 
                    self.context.semester.semester,
160
 
                    project.short_name
161
 
                    )
162
 
 
163
 
    def new_project_url(self, projectset):
164
 
        return "/api/subjects/" + self.context.subject.short_name + "/" +\
165
 
                self.context.semester.year + "/" + \
166
 
                self.context.semester.semester + "/+projectsets/" +\
167
 
                str(projectset.id) + "/+projects/+new"
168
 
    
 
588
 
169
589
    def populate(self, req, ctx):
170
590
        self.plugin_styles[Plugin] = ["project.css"]
171
591
        self.plugin_scripts[Plugin] = ["project.js"]
 
592
        ctx['req'] = req
172
593
        ctx['offering'] = self.context
173
594
        ctx['projectsets'] = []
 
595
        ctx['OfferingRESTView'] = OfferingRESTView
174
596
 
175
597
        #Open the projectset Fragment, and render it for inclusion
176
598
        #into the ProjectSets page
185
607
        for projectset in self.context.project_sets:
186
608
            settmpl = loader.load(set_fragment)
187
609
            setCtx = Context()
 
610
            setCtx['req'] = req
188
611
            setCtx['projectset'] = projectset
189
 
            setCtx['new_project_url'] = self.new_project_url(projectset)
190
612
            setCtx['projects'] = []
 
613
            setCtx['GroupsView'] = GroupsView
 
614
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
191
615
 
192
616
            for project in projectset.projects:
193
617
                projecttmpl = loader.load(project_fragment)
194
618
                projectCtx = Context()
 
619
                projectCtx['req'] = req
195
620
                projectCtx['project'] = project
196
 
                projectCtx['project_url'] = self.project_url(projectset, project)
197
621
 
198
622
                setCtx['projects'].append(
199
623
                        projecttmpl.generate(projectCtx))
204
628
class ProjectView(XHTMLView):
205
629
    """View the submissions for a ProjectSet"""
206
630
    template = "templates/project.html"
207
 
    permission = "edit"
 
631
    permission = "view_project_submissions"
208
632
    tab = 'subjects'
209
633
 
210
 
    def __init__(self, req, subject, year, semester, project):
211
 
        self.context = req.store.find(Project,
212
 
                Project.short_name == project,
213
 
                Project.project_set_id == ProjectSet.id,
214
 
                ProjectSet.offering_id == Offering.id,
215
 
                Offering.semester_id == Semester.id,
216
 
                Semester.year == year,
217
 
                Semester.semester == semester,
218
 
                Offering.subject_id == Subject.id,
219
 
                Subject.short_name == subject).one()
220
 
        if self.context is None:
221
 
            raise NotFound()
222
 
 
223
634
    def build_subversion_url(self, svnroot, submission):
224
635
        princ = submission.assessed.principal
225
636
 
241
652
    def populate(self, req, ctx):
242
653
        self.plugin_styles[Plugin] = ["project.css"]
243
654
 
 
655
        ctx['req'] = req
 
656
        ctx['GroupsView'] = GroupsView
 
657
        ctx['EnrolView'] = EnrolView
244
658
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
245
659
        ctx['build_subversion_url'] = self.build_subversion_url
246
660
        ctx['svn_addr'] = req.config['urls']['svn_addr']
248
662
        ctx['user'] = req.user
249
663
 
250
664
class Plugin(ViewPlugin, MediaPlugin):
251
 
    urls = [
252
 
        ('subjects/', SubjectsView),
253
 
        ('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
254
 
        ('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
255
 
        ('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
256
 
        #API Views
257
 
        ('api/subjects/:subject/:year/:semester/+projectsets/+new',
258
 
            OfferingRESTView),
259
 
        ('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
260
 
            ProjectSetRESTView),
261
 
        ('api/subjects/:subject/:year/:semester/+projects/:project', 
262
 
            ProjectRESTView),
263
 
 
264
 
    ]
 
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
                   }
265
694
 
266
695
    tabs = [
267
696
        ('subjects', 'Subjects',