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