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

621 by mattgiuca
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
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
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
25
import os
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
26
import os.path
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
27
import urllib
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
28
import urlparse
627 by mattgiuca
subjects: Now presents a top-level subjects menu (same as tutorials).
29
import cgi
30
1294.2.52 by William Grant
Port subject-related views to object traversal.
31
from storm.locals import Desc, Store
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
32
import genshi
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
33
from genshi.filters import HTMLFormFiller
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
34
from genshi.template import Context, TemplateLoader
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
35
import formencode
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
36
import formencode.validators
1125 by William Grant
Rework ivle.webapp.admin.subjects#SubjectsView to split offerings nicely by
37
1539 by William Grant
Move BaseFormView into ivle.webapp.base.forms.
38
from ivle.webapp.base.forms import BaseFormView
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
1099.1.34 by William Grant
Split up ivle.webapp.base.views into ivle.webapp.base.{rest,xhtml}, as it was
40
from ivle.webapp.base.xhtml import XHTMLView
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
41
from ivle.webapp.errors import BadRequest
1294.2.52 by William Grant
Port subject-related views to object traversal.
42
from ivle.webapp import ApplicationRoot
1165.3.9 by Nick Chadwick
merge from trunk
43
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
1165.3.8 by Nick Chadwick
Added a total submissions and total assesseds to the project view
45
                          ProjectSet, Project, ProjectSubmission
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
46
from ivle import util
1165.3.14 by William Grant
Improve ProjectView's template substantially.
47
import ivle.date
621 by mattgiuca
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
48
1375.1.2 by William Grant
Remove ProjectRESTView, which did nothing and was unused.
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
1294.2.70 by William Grant
Split out ivle.webapp.admin's routes into annotated functions in ivle.webapp.traversal.
52
            subject_to_offering, offering_to_projectset, offering_to_project,
1613 by William Grant
Add UI to edit/delete enrolments.
53
            offering_to_enrolment, subject_url, semester_url, offering_url,
54
            projectset_url, project_url, enrolment_url)
1294.2.96 by William Grant
Add a UserBreadcrumb.
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
1615 by William Grant
Add breadcrumbs for enrolments.
56
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
57
            EnrolmentBreadcrumb)
1533 by William Grant
Add a subject listing with new/edit icons.
58
from ivle.webapp.core import Plugin as CorePlugin
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
59
from ivle.webapp.groups import GroupsView
1533 by William Grant
Add a subject listing with new/edit icons.
60
from ivle.webapp.media import media_url
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
61
from ivle.webapp.tutorial import Plugin as TutorialPlugin
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
62
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
63
class SubjectsView(XHTMLView):
64
    '''The view of the list of subjects.'''
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
65
    template = 'templates/subjects.html'
1116 by William Grant
Move the old tutorial views into the 'subjects' tab, so they get the right
66
    tab = 'subjects'
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
67
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
68
    def authorize(self, req):
1138 by William Grant
SubjectsView now tells users if they have no enrolments.
69
        return req.user is not None
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
70
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
71
    def populate(self, req, ctx):
1525 by Matt Giuca
ivle/webapp/admin/templates/subjects.html: Use req.publisher.generate rather than rolling its own offering_url function.
72
        ctx['req'] = req
1139 by William Grant
Show group administration links on SubjectsView where privileges allow it.
73
        ctx['user'] = req.user
1125 by William Grant
Rework ivle.webapp.admin.subjects#SubjectsView to split offerings nicely by
74
        ctx['semesters'] = []
1533 by William Grant
Add a subject listing with new/edit icons.
75
1125 by William Grant
Rework ivle.webapp.admin.subjects#SubjectsView to split offerings nicely by
76
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
77
                                                     Desc(Semester.semester)):
1372 by Matt Giuca
admin/subject: Admin users now see all offerings in the system, not just the ones they are enrolled in.
78
            if req.user.admin:
79
                # For admins, show all subjects in the system
80
                offerings = list(semester.offerings.find())
81
            else:
82
                offerings = [enrolment.offering for enrolment in
1371 by Matt Giuca
admin/subject: Now sends a list of offerings the user is enrolled in to the Genshi template, rather than a list of enrolment objects (of which only the offering is observed). This allows us to send non-enrolment offerings to the template.
83
                                    semester.enrolments.find(user=req.user)]
84
            if len(offerings):
85
                ctx['semesters'].append((semester, offerings))
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
86
1596 by William Grant
Split subject/semester management out onto a separate page, and link to SemesterEdit.
87
88
class SubjectsManage(XHTMLView):
89
    '''Subject management view.'''
90
    template = 'templates/subjects-manage.html'
91
    tab = 'subjects'
92
93
    def authorize(self, req):
94
        return req.user is not None and req.user.admin
95
96
    def populate(self, req, ctx):
97
        ctx['req'] = req
98
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
99
        ctx['SubjectEdit'] = SubjectEdit
100
        ctx['SemesterEdit'] = SemesterEdit
101
102
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
103
        ctx['semesters'] = req.store.find(Semester).order_by(
104
            Semester.year, Semester.semester)
1533 by William Grant
Add a subject listing with new/edit icons.
105
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
106
107
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
108
    """A FormEncode validator that checks that a subject name is unused.
109
110
    The subject referenced by state.existing_subject is permitted
111
    to hold that name. If any other object holds it, the input is rejected.
112
    """
113
    def __init__(self, matching=None):
114
        self.matching = matching
115
116
    def _to_python(self, value, state):
117
        if (state.store.find(
118
                Subject, short_name=value).one() not in
119
                (None, state.existing_subject)):
120
            raise formencode.Invalid(
121
                'Short name already taken', value, state)
122
        return value
123
124
125
class SubjectSchema(formencode.Schema):
126
    short_name = formencode.All(
127
        SubjectShortNameUniquenessValidator(),
128
        formencode.validators.UnicodeString(not_empty=True))
129
    name = formencode.validators.UnicodeString(not_empty=True)
130
    code = formencode.validators.UnicodeString(not_empty=True)
131
132
1546 by William Grant
Derive the subject forms from BaseFormView.
133
class SubjectFormView(BaseFormView):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
134
    """An abstract form to add or edit a subject."""
135
    tab = 'subjects'
136
137
    def authorize(self, req):
138
        return req.user is not None and req.user.admin
139
140
    def populate_state(self, state):
141
        state.existing_subject = None
142
1546 by William Grant
Derive the subject forms from BaseFormView.
143
    @property
144
    def validator(self):
145
        return SubjectSchema()
146
147
    def get_return_url(self, obj):
148
        return '/subjects'
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
149
150
151
class SubjectNew(SubjectFormView):
152
    """A form to create a subject."""
153
    template = 'templates/subject-new.html'
154
155
    def get_default_data(self, req):
156
        return {}
157
1546 by William Grant
Derive the subject forms from BaseFormView.
158
    def save_object(self, req, data):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
159
        new_subject = Subject()
160
        new_subject.short_name = data['short_name']
161
        new_subject.name = data['name']
162
        new_subject.code = data['code']
163
164
        req.store.add(new_subject)
165
        return new_subject
166
167
168
class SubjectEdit(SubjectFormView):
169
    """A form to edit a subject."""
170
    template = 'templates/subject-edit.html'
171
172
    def populate_state(self, state):
173
        state.existing_subject = self.context
174
175
    def get_default_data(self, req):
176
        return {
177
            'short_name': self.context.short_name,
178
            'name': self.context.name,
179
            'code': self.context.code,
180
            }
181
1546 by William Grant
Derive the subject forms from BaseFormView.
182
    def save_object(self, req, data):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
183
        self.context.short_name = data['short_name']
184
        self.context.name = data['name']
185
        self.context.code = data['code']
186
187
        return self.context
188
189
1543 by William Grant
Semester creation UI.
190
class SemesterUniquenessValidator(formencode.FancyValidator):
191
    """A FormEncode validator that checks that a semester is unique.
192
193
    There cannot be more than one semester for the same year and semester.
194
    """
195
    def _to_python(self, value, state):
196
        if (state.store.find(
197
                Semester, year=value['year'], semester=value['semester']
1594 by William Grant
Add semester edit UI.
198
                ).one() not in (None, state.existing_semester)):
1543 by William Grant
Semester creation UI.
199
            raise formencode.Invalid(
200
                'Semester already exists', value, state)
201
        return value
202
203
204
class SemesterSchema(formencode.Schema):
205
    year = formencode.validators.UnicodeString()
206
    semester = formencode.validators.UnicodeString()
1594 by William Grant
Add semester edit UI.
207
    state = formencode.All(
208
        formencode.validators.OneOf(["past", "current", "future"]),
209
        formencode.validators.UnicodeString())
1543 by William Grant
Semester creation UI.
210
    chained_validators = [SemesterUniquenessValidator()]
211
212
1594 by William Grant
Add semester edit UI.
213
class SemesterFormView(BaseFormView):
214
    tab = 'subjects'
215
216
    def authorize(self, req):
217
        return req.user is not None and req.user.admin
218
219
    @property
220
    def validator(self):
221
        return SemesterSchema()
222
223
    def get_return_url(self, obj):
224
        return '/subjects/+manage'
225
226
227
class SemesterNew(SemesterFormView):
1543 by William Grant
Semester creation UI.
228
    """A form to create a semester."""
229
    template = 'templates/semester-new.html'
230
    tab = 'subjects'
231
1594 by William Grant
Add semester edit UI.
232
    def populate_state(self, state):
233
        state.existing_semester = None
1543 by William Grant
Semester creation UI.
234
235
    def get_default_data(self, req):
236
        return {}
237
238
    def save_object(self, req, data):
239
        new_semester = Semester()
240
        new_semester.year = data['year']
241
        new_semester.semester = data['semester']
1594 by William Grant
Add semester edit UI.
242
        new_semester.state = data['state']
1543 by William Grant
Semester creation UI.
243
244
        req.store.add(new_semester)
245
        return new_semester
246
1594 by William Grant
Add semester edit UI.
247
248
class SemesterEdit(SemesterFormView):
249
    """A form to edit a semester."""
250
    template = 'templates/semester-edit.html'
251
252
    def populate_state(self, state):
253
        state.existing_semester = self.context
254
255
    def get_default_data(self, req):
256
        return {
257
            'year': self.context.year,
258
            'semester': self.context.semester,
259
            'state': self.context.state,
260
            }
261
262
    def save_object(self, req, data):
263
        self.context.year = data['year']
264
        self.context.semester = data['semester']
265
        self.context.state = data['state']
266
267
        return self.context
1543 by William Grant
Semester creation UI.
268
269
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
270
class OfferingView(XHTMLView):
271
    """The home page of an offering."""
272
    template = 'templates/offering.html'
273
    tab = 'subjects'
274
    permission = 'view'
275
276
    def populate(self, req, ctx):
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
277
        # Need the worksheet result styles.
278
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
279
        ctx['context'] = self.context
280
        ctx['req'] = req
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
281
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
1515 by Matt Giuca
Submit view: The projects list is now identical (except for radio buttons) to the view on the subjects page. It is much clearer and contains more info. The code is somewhat different, because it's a table, not a list, so I didn't abstract it. Moved a function out of subject.py to ivle.util, as it is shared by both views.
282
        ctx['format_submission_principal'] = util.format_submission_principal
1442.1.10 by William Grant
Add a nice padded list of projects.
283
        ctx['format_datetime'] = ivle.date.make_date_nice
284
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
1451.1.7 by William Grant
Add a 'Change details' link on the offering index, pointing to +edit.
285
        ctx['OfferingEdit'] = OfferingEdit
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
286
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
1558 by William Grant
Allow tutors to manage groups.
287
        ctx['GroupsView'] = GroupsView
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
288
        ctx['EnrolmentsView'] = EnrolmentsView
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
289
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
290
        # As we go, calculate the total score for this subject
291
        # (Assessable worksheets only, mandatory problems only)
292
293
        ctx['worksheets'], problems_total, problems_done = (
294
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
295
                req.store, req.user, self.context))
296
297
        ctx['exercises_total'] = problems_total
298
        ctx['exercises_done'] = problems_done
299
        if problems_total > 0:
300
            if problems_done >= problems_total:
301
                ctx['worksheets_complete_class'] = "complete"
302
            elif problems_done > 0:
303
                ctx['worksheets_complete_class'] = "semicomplete"
304
            else:
305
                ctx['worksheets_complete_class'] = "incomplete"
306
            # Calculate the final percentage and mark for the subject
307
            (ctx['exercises_pct'], ctx['worksheet_mark'],
308
             ctx['worksheet_max_mark']) = (
309
                ivle.worksheet.utils.calculate_mark(
310
                    problems_done, problems_total))
311
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
312
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
313
class SubjectValidator(formencode.FancyValidator):
314
    """A FormEncode validator that turns a subject name into a subject.
315
316
    The state must have a 'store' attribute, which is the Storm store
317
    to use.
318
    """
319
    def _to_python(self, value, state):
320
        subject = state.store.find(Subject, short_name=value).one()
321
        if subject:
322
            return subject
323
        else:
324
            raise formencode.Invalid('Subject does not exist', value, state)
325
326
327
class SemesterValidator(formencode.FancyValidator):
328
    """A FormEncode validator that turns a string into a semester.
329
330
    The string should be of the form 'year/semester', eg. '2009/1'.
331
332
    The state must have a 'store' attribute, which is the Storm store
333
    to use.
334
    """
335
    def _to_python(self, value, state):
336
        try:
337
            year, semester = value.split('/')
338
        except ValueError:
339
            year = semester = None
340
341
        semester = state.store.find(
342
            Semester, year=year, semester=semester).one()
343
        if semester:
344
            return semester
345
        else:
346
            raise formencode.Invalid('Semester does not exist', value, state)
347
348
349
class OfferingUniquenessValidator(formencode.FancyValidator):
350
    """A FormEncode validator that checks that an offering is unique.
351
352
    There cannot be more than one offering in the same year and semester.
353
354
    The offering referenced by state.existing_offering is permitted to
355
    hold that year and semester tuple. If any other object holds it, the
356
    input is rejected.
357
    """
358
    def _to_python(self, value, state):
359
        if (state.store.find(
360
                Offering, subject=value['subject'],
361
                semester=value['semester']).one() not in
362
                (None, state.existing_offering)):
363
            raise formencode.Invalid(
364
                'Offering already exists', value, state)
365
        return value
366
367
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
368
class OfferingSchema(formencode.Schema):
1451.1.8 by William Grant
Allow unsetting of the URL or description.
369
    description = formencode.validators.UnicodeString(
370
        if_missing=None, not_empty=False)
371
    url = formencode.validators.URL(if_missing=None, not_empty=False)
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
372
373
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
374
class OfferingAdminSchema(OfferingSchema):
375
    subject = formencode.All(
376
        SubjectValidator(), formencode.validators.UnicodeString())
377
    semester = formencode.All(
378
        SemesterValidator(), formencode.validators.UnicodeString())
379
    chained_validators = [OfferingUniquenessValidator()]
380
381
382
class OfferingEdit(BaseFormView):
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
383
    """A form to edit an offering's details."""
384
    template = 'templates/offering-edit.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
385
    tab = 'subjects'
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
386
    permission = 'edit'
387
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
388
    @property
389
    def validator(self):
390
        if self.req.user.admin:
391
            return OfferingAdminSchema()
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
392
        else:
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
393
            return OfferingSchema()
394
395
    def populate(self, req, ctx):
396
        super(OfferingEdit, self).populate(req, ctx)
1598 by William Grant
Sort subjects and semesters sanely in the offering forms.
397
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
398
        ctx['semesters'] = req.store.find(Semester).order_by(
399
            Semester.year, Semester.semester)
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
400
401
    def populate_state(self, state):
402
        state.existing_offering = self.context
403
404
    def get_default_data(self, req):
405
        return {
406
            'subject': self.context.subject.short_name,
407
            'semester': self.context.semester.year + '/' +
408
                        self.context.semester.semester,
409
            'url': self.context.url,
410
            'description': self.context.description,
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
411
            }
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
412
413
    def save_object(self, req, data):
414
        if req.user.admin:
415
            self.context.subject = data['subject']
416
            self.context.semester = data['semester']
417
        self.context.description = data['description']
418
        self.context.url = unicode(data['url']) if data['url'] else None
419
        return self.context
420
421
422
class OfferingNew(BaseFormView):
423
    """A form to create an offering."""
424
    template = 'templates/offering-new.html'
425
    tab = 'subjects'
426
427
    def authorize(self, req):
428
        return req.user is not None and req.user.admin
429
430
    @property
431
    def validator(self):
432
        return OfferingAdminSchema()
433
434
    def populate(self, req, ctx):
435
        super(OfferingNew, self).populate(req, ctx)
1599 by William Grant
Sort subjects and semesters sanely in the offering new form too.
436
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
437
        ctx['semesters'] = req.store.find(Semester).order_by(
438
            Semester.year, Semester.semester)
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
439
440
    def populate_state(self, state):
441
        state.existing_offering = None
442
443
    def get_default_data(self, req):
444
        return {}
445
446
    def save_object(self, req, data):
447
        new_offering = Offering()
448
        new_offering.subject = data['subject']
449
        new_offering.semester = data['semester']
450
        new_offering.description = data['description']
451
        new_offering.url = unicode(data['url']) if data['url'] else None
452
453
        req.store.add(new_offering)
454
        return new_offering
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
455
456
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
457
class OfferingCloneWorksheetsSchema(formencode.Schema):
458
    subject = formencode.All(
459
        SubjectValidator(), formencode.validators.UnicodeString())
460
    semester = formencode.All(
461
        SemesterValidator(), formencode.validators.UnicodeString())
462
463
464
class OfferingCloneWorksheets(BaseFormView):
465
    """A form to clone worksheets from one offering to another."""
466
    template = 'templates/offering-clone-worksheets.html'
467
    tab = 'subjects'
468
469
    def authorize(self, req):
470
        return req.user is not None and req.user.admin
471
472
    @property
473
    def validator(self):
474
        return OfferingCloneWorksheetsSchema()
475
476
    def populate(self, req, ctx):
477
        super(OfferingCloneWorksheets, self).populate(req, ctx)
478
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
479
        ctx['semesters'] = req.store.find(Semester).order_by(
480
            Semester.year, Semester.semester)
481
482
    def get_default_data(self, req):
483
        return {}
484
485
    def save_object(self, req, data):
486
        if self.context.worksheets.count() > 0:
487
            raise BadRequest(
488
                "Cannot clone to target with existing worksheets.")
489
        offering = req.store.find(
490
            Offering, subject=data['subject'], semester=data['semester']).one()
491
        if offering is None:
492
            raise BadRequest("No such offering.")
493
        if offering.worksheets.count() == 0:
494
            raise BadRequest("Source offering has no worksheets.")
495
496
        self.context.clone_worksheets(offering)
497
        return self.context
498
499
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
500
class UserValidator(formencode.FancyValidator):
501
    """A FormEncode validator that turns a username into a user.
502
503
    The state must have a 'store' attribute, which is the Storm store
504
    to use."""
505
    def _to_python(self, value, state):
506
        user = User.get_by_login(state.store, value)
507
        if user:
508
            return user
509
        else:
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
510
            raise formencode.Invalid('User does not exist', value, state)
511
512
513
class NoEnrolmentValidator(formencode.FancyValidator):
514
    """A FormEncode validator that ensures absence of an enrolment.
515
516
    The state must have an 'offering' attribute.
517
    """
518
    def _to_python(self, value, state):
519
        if state.offering.get_enrolment(value):
520
            raise formencode.Invalid('User already enrolled', value, state)
521
        return value
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
522
523
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
524
class RoleEnrolmentValidator(formencode.FancyValidator):
525
    """A FormEncode validator that checks permission to enrol users with a
526
    particular role.
527
528
    The state must have an 'offering' attribute.
529
    """
530
    def _to_python(self, value, state):
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
531
        if (("enrol_" + value) not in
532
                state.offering.get_permissions(state.user, state.config)):
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
533
            raise formencode.Invalid('Not allowed to assign users that role',
534
                                     value, state)
535
        return value
536
537
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
538
class EnrolSchema(formencode.Schema):
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
539
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
540
    role = formencode.All(formencode.validators.OneOf(
541
                                ["lecturer", "tutor", "student"]),
542
                          RoleEnrolmentValidator(),
543
                          formencode.validators.UnicodeString())
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
544
545
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
546
class EnrolmentsView(XHTMLView):
547
    """A page which displays all users enrolled in an offering."""
548
    template = 'templates/enrolments.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
549
    tab = 'subjects'
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
550
    permission = 'edit'
1615 by William Grant
Add breadcrumbs for enrolments.
551
    breadcrumb_text = 'Enrolments'
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
552
553
    def populate(self, req, ctx):
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
554
        ctx['req'] = req
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
555
        ctx['offering'] = self.context
1613 by William Grant
Add UI to edit/delete enrolments.
556
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
1614 by William Grant
Only show edit/delete links for enrolments that you can actually touch.
557
        ctx['offering_perms'] = self.context.get_permissions(
558
            req.user, req.config)
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
559
        ctx['EnrolView'] = EnrolView
1613 by William Grant
Add UI to edit/delete enrolments.
560
        ctx['EnrolmentEdit'] = EnrolmentEdit
561
        ctx['EnrolmentDelete'] = EnrolmentDelete
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
562
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
563
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
564
class EnrolView(XHTMLView):
565
    """A form to enrol a user in an offering."""
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
566
    template = 'templates/enrol.html'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
567
    tab = 'subjects'
1376 by Matt Giuca
database: More granular permissions on offerings: Added 'enrol' permission.
568
    permission = 'enrol'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
569
570
    def filter(self, stream, ctx):
571
        return stream | HTMLFormFiller(data=ctx['data'])
572
573
    def populate(self, req, ctx):
574
        if req.method == 'POST':
575
            data = dict(req.get_fieldstorage())
576
            try:
577
                validator = EnrolSchema()
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
578
                req.offering = self.context # XXX: Getting into state.
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
579
                data = validator.to_python(data, state=req)
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
580
                self.context.enrol(data['user'], data['role'])
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
581
                req.store.commit()
582
                req.throw_redirect(req.uri)
583
            except formencode.Invalid, e:
584
                errors = e.unpack_errors()
585
        else:
586
            data = {}
587
            errors = {}
588
589
        ctx['data'] = data or {}
590
        ctx['offering'] = self.context
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
591
        ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
592
        ctx['errors'] = errors
593
1613 by William Grant
Add UI to edit/delete enrolments.
594
595
class EnrolmentEditSchema(formencode.Schema):
596
    role = formencode.All(formencode.validators.OneOf(
597
                                ["lecturer", "tutor", "student"]),
598
                          RoleEnrolmentValidator(),
599
                          formencode.validators.UnicodeString())
600
601
602
class EnrolmentEdit(BaseFormView):
603
    """A form to alter an enrolment's role."""
604
    template = 'templates/enrolment-edit.html'
605
    tab = 'subjects'
606
    permission = 'edit'
607
608
    def populate_state(self, state):
609
        state.offering = self.context.offering
610
611
    def get_default_data(self, req):
612
        return {'role': self.context.role}
613
614
    @property
615
    def validator(self):
616
        return EnrolmentEditSchema()
617
618
    def save_object(self, req, data):
619
        self.context.role = data['role']
620
621
    def get_return_url(self, obj):
622
        return self.req.publisher.generate(
623
            self.context.offering, EnrolmentsView)
624
625
    def populate(self, req, ctx):
626
        super(EnrolmentEdit, self).populate(req, ctx)
627
        ctx['offering_perms'] = self.context.offering.get_permissions(
628
            req.user, req.config)
629
630
631
class EnrolmentDelete(XHTMLView):
632
    """A form to alter an enrolment's role."""
633
    template = 'templates/enrolment-delete.html'
634
    tab = 'subjects'
635
    permission = 'edit'
636
637
    def populate(self, req, ctx):
638
        # If POSTing, delete delete delete.
639
        if req.method == 'POST':
640
            self.context.delete()
641
            req.store.commit()
642
            req.throw_redirect(req.publisher.generate(
643
                self.context.offering, EnrolmentsView))
644
645
        ctx['enrolment'] = self.context
646
647
1165.3.19 by William Grant
Rename SubjectProjectSetView to OfferingProjectsView.
648
class OfferingProjectsView(XHTMLView):
649
    """View the projects for an offering."""
650
    template = 'templates/offering_projects.html'
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
651
    permission = 'edit'
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
652
    tab = 'subjects'
1616 by William Grant
Add a Projects breadcrumb.
653
    breadcrumb_text = 'Projects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
654
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
655
    def populate(self, req, ctx):
656
        self.plugin_styles[Plugin] = ["project.css"]
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
657
        self.plugin_scripts[Plugin] = ["project.js"]
1361 by William Grant
Remove the last +projectsets hardcoding.
658
        ctx['req'] = req
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
659
        ctx['offering'] = self.context
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
660
        ctx['projectsets'] = []
1361 by William Grant
Remove the last +projectsets hardcoding.
661
        ctx['OfferingRESTView'] = OfferingRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
662
663
        #Open the projectset Fragment, and render it for inclusion
664
        #into the ProjectSets page
665
        #XXX: This could be a lot cleaner
666
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
667
668
        set_fragment = os.path.join(os.path.dirname(__file__),
669
                "templates/projectset_fragment.html")
670
        project_fragment = os.path.join(os.path.dirname(__file__),
671
                "templates/project_fragment.html")
672
673
        for projectset in self.context.project_sets:
674
            settmpl = loader.load(set_fragment)
675
            setCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
676
            setCtx['req'] = req
1165.3.30 by William Grant
Clean out the projectset fragment context.
677
            setCtx['projectset'] = projectset
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
678
            setCtx['projects'] = []
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
679
            setCtx['GroupsView'] = GroupsView
680
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
681
682
            for project in projectset.projects:
683
                projecttmpl = loader.load(project_fragment)
684
                projectCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
685
                projectCtx['req'] = req
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
686
                projectCtx['project'] = project
687
688
                setCtx['projects'].append(
689
                        projecttmpl.generate(projectCtx))
690
691
            ctx['projectsets'].append(settmpl.generate(setCtx))
692
693
694
class ProjectView(XHTMLView):
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
695
    """View the submissions for a ProjectSet"""
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
696
    template = "templates/project.html"
1556 by William Grant
Allow tutors to view project submissions.
697
    permission = "view_project_submissions"
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
698
    tab = 'subjects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
699
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
700
    def build_subversion_url(self, svnroot, submission):
701
        princ = submission.assessed.principal
702
703
        if isinstance(princ, User):
704
            path = 'users/%s' % princ.login
705
        else:
706
            path = 'groups/%s_%s_%s_%s' % (
707
                    princ.project_set.offering.subject.short_name,
708
                    princ.project_set.offering.semester.year,
709
                    princ.project_set.offering.semester.semester,
710
                    princ.name
711
                    )
712
        return urlparse.urljoin(
713
                    svnroot,
714
                    os.path.join(path, submission.path[1:] if
715
                                       submission.path.startswith(os.sep) else
716
                                       submission.path))
717
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
718
    def populate(self, req, ctx):
1165.3.66 by William Grant
Prettify the submissions table.
719
        self.plugin_styles[Plugin] = ["project.css"]
720
1375.1.4 by William Grant
Indicate when there is nobody assigned to a project, and link to the page to fix that.
721
        ctx['req'] = req
722
        ctx['GroupsView'] = GroupsView
723
        ctx['EnrolView'] = EnrolView
1165.3.14 by William Grant
Improve ProjectView's template substantially.
724
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
725
        ctx['build_subversion_url'] = self.build_subversion_url
726
        ctx['svn_addr'] = req.config['urls']['svn_addr']
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
727
        ctx['project'] = self.context
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
728
        ctx['user'] = req.user
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
729
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
730
class Plugin(ViewPlugin, MediaPlugin):
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
731
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
1613 by William Grant
Add UI to edit/delete enrolments.
732
                      offering_to_project, offering_to_projectset,
733
                      offering_to_enrolment)
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
734
    reverse_routes = (
1613 by William Grant
Add UI to edit/delete enrolments.
735
        subject_url, semester_url, offering_url, projectset_url, project_url,
736
        enrolment_url)
1294.2.52 by William Grant
Port subject-related views to object traversal.
737
738
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
1596 by William Grant
Split subject/semester management out onto a separate page, and link to SemesterEdit.
739
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
740
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
741
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
1594 by William Grant
Add semester edit UI.
742
             (ApplicationRoot, ('+semesters', '+new'), SemesterNew),
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
743
             (Subject, '+edit', SubjectEdit),
1594 by William Grant
Add semester edit UI.
744
             (Semester, '+edit', SemesterEdit),
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
745
             (Offering, '+index', OfferingView),
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
746
             (Offering, '+edit', OfferingEdit),
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
747
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
748
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
1294.2.52 by William Grant
Port subject-related views to object traversal.
749
             (Offering, ('+enrolments', '+new'), EnrolView),
1613 by William Grant
Add UI to edit/delete enrolments.
750
             (Enrolment, '+edit', EnrolmentEdit),
751
             (Enrolment, '+delete', EnrolmentDelete),
1294.2.52 by William Grant
Port subject-related views to object traversal.
752
             (Offering, ('+projects', '+index'), OfferingProjectsView),
753
             (Project, '+index', ProjectView),
754
755
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
756
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
757
             ]
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
758
1294.2.94 by William Grant
Add a SubjectBreadcrumb.
759
    breadcrumbs = {Subject: SubjectBreadcrumb,
760
                   Offering: OfferingBreadcrumb,
1294.2.96 by William Grant
Add a UserBreadcrumb.
761
                   User: UserBreadcrumb,
1294.2.98 by William Grant
Add a ProjectBreadcrumb.
762
                   Project: ProjectBreadcrumb,
1615 by William Grant
Add breadcrumbs for enrolments.
763
                   Enrolment: EnrolmentBreadcrumb,
1294.2.89 by William Grant
Add an Offering breadcrumb.
764
                   }
765
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
766
    tabs = [
1118 by matt.giuca
Rewrote tooltips for the four tabs visible by default.
767
        ('subjects', 'Subjects',
768
         'View subject content and complete worksheets',
769
         'subjects.png', 'subjects', 5)
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
770
    ]
771
772
    media = 'subject-media'