~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
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
34
from genshi.template import Context
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
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
38
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
39
                                    DateTimeValidator)
1539 by William Grant
Move BaseFormView into ivle.webapp.base.forms.
40
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
41
from ivle.webapp.base.xhtml import XHTMLView
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
42
from ivle.webapp.errors import BadRequest
1294.2.52 by William Grant
Port subject-related views to object traversal.
43
from ivle.webapp import ApplicationRoot
1165.3.9 by Nick Chadwick
merge from trunk
44
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
45
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
46
                          ProjectSet, Project, ProjectSubmission
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
47
from ivle import util
1165.3.14 by William Grant
Improve ProjectView's template substantially.
48
import ivle.date
621 by mattgiuca
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
49
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
50
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.
51
            subject_to_offering, offering_to_projectset, offering_to_project,
1613 by William Grant
Add UI to edit/delete enrolments.
52
            offering_to_enrolment, subject_url, semester_url, offering_url,
53
            projectset_url, project_url, enrolment_url)
1294.2.96 by William Grant
Add a UserBreadcrumb.
54
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
1615 by William Grant
Add breadcrumbs for enrolments.
55
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
1710.1.5 by Matt Giuca
Added new project set edit view. Linked from projects page, project set page.
56
            ProjectsBreadcrumb, EnrolmentBreadcrumb)
1533 by William Grant
Add a subject listing with new/edit icons.
57
from ivle.webapp.core import Plugin as CorePlugin
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
58
from ivle.webapp.groups import GroupsView
1533 by William Grant
Add a subject listing with new/edit icons.
59
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.
60
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
61
1099.1.20 by William Grant
ivle.webapp.admin.subject: Port www/apps/subjects to new framework.
62
class SubjectsView(XHTMLView):
63
    '''The view of the list of subjects.'''
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
64
    template = 'templates/subjects.html'
1116 by William Grant
Move the old tutorial views into the 'subjects' tab, so they get the right
65
    tab = 'subjects'
1683 by Matt Giuca
Added breadcrumb for Subjects page (previously each subject had its own top-level breadcrumb).
66
    breadcrumb_text = "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/')
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.
99
        ctx['SubjectView'] = SubjectView
1596 by William Grant
Split subject/semester management out onto a separate page, and link to SemesterEdit.
100
        ctx['SubjectEdit'] = SubjectEdit
101
        ctx['SemesterEdit'] = SemesterEdit
102
103
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
104
        ctx['semesters'] = req.store.find(Semester).order_by(
105
            Semester.year, Semester.semester)
1533 by William Grant
Add a subject listing with new/edit icons.
106
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
107
108
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
109
    """A FormEncode validator that checks that a subject name is unused.
110
111
    The subject referenced by state.existing_subject is permitted
112
    to hold that name. If any other object holds it, the input is rejected.
113
    """
114
    def __init__(self, matching=None):
115
        self.matching = matching
116
117
    def _to_python(self, value, state):
118
        if (state.store.find(
119
                Subject, short_name=value).one() not in
120
                (None, state.existing_subject)):
121
            raise formencode.Invalid(
122
                'Short name already taken', value, state)
123
        return value
124
125
126
class SubjectSchema(formencode.Schema):
127
    short_name = formencode.All(
128
        SubjectShortNameUniquenessValidator(),
1606.1.3 by William Grant
Use URLNameValidator in existing schemas.
129
        URLNameValidator(not_empty=True))
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
130
    name = formencode.validators.UnicodeString(not_empty=True)
131
    code = formencode.validators.UnicodeString(not_empty=True)
132
133
1546 by William Grant
Derive the subject forms from BaseFormView.
134
class SubjectFormView(BaseFormView):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
135
    """An abstract form to add or edit a subject."""
136
    tab = 'subjects'
137
138
    def authorize(self, req):
139
        return req.user is not None and req.user.admin
140
141
    def populate_state(self, state):
142
        state.existing_subject = None
143
1546 by William Grant
Derive the subject forms from BaseFormView.
144
    @property
145
    def validator(self):
146
        return SubjectSchema()
147
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
148
149
class SubjectNew(SubjectFormView):
150
    """A form to create a subject."""
151
    template = 'templates/subject-new.html'
152
153
    def get_default_data(self, req):
154
        return {}
155
1546 by William Grant
Derive the subject forms from BaseFormView.
156
    def save_object(self, req, data):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
157
        new_subject = Subject()
158
        new_subject.short_name = data['short_name']
159
        new_subject.name = data['name']
160
        new_subject.code = data['code']
161
162
        req.store.add(new_subject)
163
        return new_subject
164
165
166
class SubjectEdit(SubjectFormView):
167
    """A form to edit a subject."""
168
    template = 'templates/subject-edit.html'
169
170
    def populate_state(self, state):
171
        state.existing_subject = self.context
172
173
    def get_default_data(self, req):
174
        return {
175
            'short_name': self.context.short_name,
176
            'name': self.context.name,
177
            'code': self.context.code,
178
            }
179
1546 by William Grant
Derive the subject forms from BaseFormView.
180
    def save_object(self, req, data):
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
181
        self.context.short_name = data['short_name']
182
        self.context.name = data['name']
183
        self.context.code = data['code']
184
185
        return self.context
186
187
1543 by William Grant
Semester creation UI.
188
class SemesterUniquenessValidator(formencode.FancyValidator):
189
    """A FormEncode validator that checks that a semester is unique.
190
191
    There cannot be more than one semester for the same year and semester.
192
    """
193
    def _to_python(self, value, state):
194
        if (state.store.find(
195
                Semester, year=value['year'], semester=value['semester']
1594 by William Grant
Add semester edit UI.
196
                ).one() not in (None, state.existing_semester)):
1543 by William Grant
Semester creation UI.
197
            raise formencode.Invalid(
198
                'Semester already exists', value, state)
199
        return value
200
201
202
class SemesterSchema(formencode.Schema):
1606.1.3 by William Grant
Use URLNameValidator in existing schemas.
203
    year = URLNameValidator()
204
    semester = URLNameValidator()
1594 by William Grant
Add semester edit UI.
205
    state = formencode.All(
206
        formencode.validators.OneOf(["past", "current", "future"]),
207
        formencode.validators.UnicodeString())
1543 by William Grant
Semester creation UI.
208
    chained_validators = [SemesterUniquenessValidator()]
209
210
1594 by William Grant
Add semester edit UI.
211
class SemesterFormView(BaseFormView):
212
    tab = 'subjects'
213
214
    def authorize(self, req):
215
        return req.user is not None and req.user.admin
216
217
    @property
218
    def validator(self):
219
        return SemesterSchema()
220
221
    def get_return_url(self, obj):
222
        return '/subjects/+manage'
223
224
225
class SemesterNew(SemesterFormView):
1543 by William Grant
Semester creation UI.
226
    """A form to create a semester."""
227
    template = 'templates/semester-new.html'
228
    tab = 'subjects'
229
1594 by William Grant
Add semester edit UI.
230
    def populate_state(self, state):
231
        state.existing_semester = None
1543 by William Grant
Semester creation UI.
232
233
    def get_default_data(self, req):
234
        return {}
235
236
    def save_object(self, req, data):
237
        new_semester = Semester()
238
        new_semester.year = data['year']
239
        new_semester.semester = data['semester']
1594 by William Grant
Add semester edit UI.
240
        new_semester.state = data['state']
1543 by William Grant
Semester creation UI.
241
242
        req.store.add(new_semester)
243
        return new_semester
244
1594 by William Grant
Add semester edit UI.
245
246
class SemesterEdit(SemesterFormView):
247
    """A form to edit a semester."""
248
    template = 'templates/semester-edit.html'
249
250
    def populate_state(self, state):
251
        state.existing_semester = self.context
252
253
    def get_default_data(self, req):
254
        return {
255
            'year': self.context.year,
256
            'semester': self.context.semester,
257
            'state': self.context.state,
258
            }
259
260
    def save_object(self, req, data):
261
        self.context.year = data['year']
262
        self.context.semester = data['semester']
263
        self.context.state = data['state']
264
265
        return self.context
1543 by William Grant
Semester creation UI.
266
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.
267
class SubjectView(XHTMLView):
268
    '''The view of the list of offerings in a given subject.'''
269
    template = 'templates/subject.html'
270
    tab = 'subjects'
271
272
    def authorize(self, req):
273
        return req.user is not None
274
275
    def populate(self, req, ctx):
276
        ctx['context'] = self.context
277
        ctx['req'] = req
278
        ctx['user'] = req.user
279
        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.
280
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
281
        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.
282
        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.
283
1543 by William Grant
Semester creation UI.
284
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
285
class OfferingView(XHTMLView):
286
    """The home page of an offering."""
287
    template = 'templates/offering.html'
288
    tab = 'subjects'
289
    permission = 'view'
290
291
    def populate(self, req, ctx):
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
292
        # Need the worksheet result styles.
293
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
294
        ctx['context'] = self.context
295
        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.
296
        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.
297
        ctx['format_submission_principal'] = util.format_submission_principal
1442.1.10 by William Grant
Add a nice padded list of projects.
298
        ctx['format_datetime'] = ivle.date.make_date_nice
299
        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.
300
        ctx['OfferingEdit'] = OfferingEdit
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
301
        ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
1558 by William Grant
Allow tutors to manage groups.
302
        ctx['GroupsView'] = GroupsView
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
303
        ctx['EnrolmentsView'] = EnrolmentsView
1725 by Matt Giuca
Subject offering page: Sort projects by deadline (for student view).
304
        ctx['Project'] = ivle.database.Project
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(
1739 by Matt Giuca
Offering view now respects worksheet_cutoff when displaying the student's mark. Currently also the individual worksheet completions stop at the cutoff. This can be changed, but is hard.
311
                req.config, req.store, req.user, self.context,
312
                as_of=self.context.worksheet_cutoff))
1442.1.31 by William Grant
Show the worksheet listing with marks and schtuff on the offering index.
313
314
        ctx['exercises_total'] = problems_total
315
        ctx['exercises_done'] = problems_done
316
        if problems_total > 0:
317
            if problems_done >= problems_total:
318
                ctx['worksheets_complete_class'] = "complete"
319
            elif problems_done > 0:
320
                ctx['worksheets_complete_class'] = "semicomplete"
321
            else:
322
                ctx['worksheets_complete_class'] = "incomplete"
323
            # Calculate the final percentage and mark for the subject
324
            (ctx['exercises_pct'], ctx['worksheet_mark'],
325
             ctx['worksheet_max_mark']) = (
326
                ivle.worksheet.utils.calculate_mark(
327
                    problems_done, problems_total))
328
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
329
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
330
class SubjectValidator(formencode.FancyValidator):
331
    """A FormEncode validator that turns a subject name into a subject.
332
333
    The state must have a 'store' attribute, which is the Storm store
334
    to use.
335
    """
336
    def _to_python(self, value, state):
337
        subject = state.store.find(Subject, short_name=value).one()
338
        if subject:
339
            return subject
340
        else:
341
            raise formencode.Invalid('Subject does not exist', value, state)
342
343
344
class SemesterValidator(formencode.FancyValidator):
345
    """A FormEncode validator that turns a string into a semester.
346
347
    The string should be of the form 'year/semester', eg. '2009/1'.
348
349
    The state must have a 'store' attribute, which is the Storm store
350
    to use.
351
    """
352
    def _to_python(self, value, state):
353
        try:
354
            year, semester = value.split('/')
355
        except ValueError:
356
            year = semester = None
357
358
        semester = state.store.find(
359
            Semester, year=year, semester=semester).one()
360
        if semester:
361
            return semester
362
        else:
363
            raise formencode.Invalid('Semester does not exist', value, state)
364
365
366
class OfferingUniquenessValidator(formencode.FancyValidator):
367
    """A FormEncode validator that checks that an offering is unique.
368
369
    There cannot be more than one offering in the same year and semester.
370
371
    The offering referenced by state.existing_offering is permitted to
372
    hold that year and semester tuple. If any other object holds it, the
373
    input is rejected.
374
    """
375
    def _to_python(self, value, state):
376
        if (state.store.find(
377
                Offering, subject=value['subject'],
378
                semester=value['semester']).one() not in
379
                (None, state.existing_offering)):
380
            raise formencode.Invalid(
381
                'Offering already exists', value, state)
382
        return value
383
384
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
385
class OfferingSchema(formencode.Schema):
1451.1.8 by William Grant
Allow unsetting of the URL or description.
386
    description = formencode.validators.UnicodeString(
387
        if_missing=None, not_empty=False)
388
    url = formencode.validators.URL(if_missing=None, not_empty=False)
1736 by Matt Giuca
OfferingEdit: Worksheet cutoff is now allowed to be empty.
389
    worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
1695.1.4 by William Grant
Expose Offering.show_worksheet_marks in the forms.
390
    show_worksheet_marks = formencode.validators.StringBoolean(
391
        if_missing=False)
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
392
393
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
394
class OfferingAdminSchema(OfferingSchema):
395
    subject = formencode.All(
396
        SubjectValidator(), formencode.validators.UnicodeString())
397
    semester = formencode.All(
398
        SemesterValidator(), formencode.validators.UnicodeString())
399
    chained_validators = [OfferingUniquenessValidator()]
400
401
402
class OfferingEdit(BaseFormView):
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
403
    """A form to edit an offering's details."""
404
    template = 'templates/offering-edit.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
405
    tab = 'subjects'
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
406
    permission = 'edit'
407
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
408
    @property
409
    def validator(self):
410
        if self.req.user.admin:
411
            return OfferingAdminSchema()
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
412
        else:
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
413
            return OfferingSchema()
414
415
    def populate(self, req, ctx):
416
        super(OfferingEdit, self).populate(req, ctx)
1598 by William Grant
Sort subjects and semesters sanely in the offering forms.
417
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
418
        ctx['semesters'] = req.store.find(Semester).order_by(
419
            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.
420
        ctx['force_subject'] = None
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
421
422
    def populate_state(self, state):
423
        state.existing_offering = self.context
424
425
    def get_default_data(self, req):
426
        return {
427
            'subject': self.context.subject.short_name,
428
            'semester': self.context.semester.year + '/' +
429
                        self.context.semester.semester,
430
            'url': self.context.url,
431
            'description': self.context.description,
1734 by Matt Giuca
OfferingEdit view: Added Worksheets cutoff field.
432
            'worksheet_cutoff': self.context.worksheet_cutoff,
1695.1.4 by William Grant
Expose Offering.show_worksheet_marks in the forms.
433
            'show_worksheet_marks': self.context.show_worksheet_marks,
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
434
            }
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
435
436
    def save_object(self, req, data):
437
        if req.user.admin:
438
            self.context.subject = data['subject']
439
            self.context.semester = data['semester']
440
        self.context.description = data['description']
441
        self.context.url = unicode(data['url']) if data['url'] else None
1734 by Matt Giuca
OfferingEdit view: Added Worksheets cutoff field.
442
        self.context.worksheet_cutoff = data['worksheet_cutoff']
1695.1.4 by William Grant
Expose Offering.show_worksheet_marks in the forms.
443
        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.
444
        return self.context
445
446
447
class OfferingNew(BaseFormView):
448
    """A form to create an offering."""
449
    template = 'templates/offering-new.html'
450
    tab = 'subjects'
451
452
    def authorize(self, req):
453
        return req.user is not None and req.user.admin
454
455
    @property
456
    def validator(self):
457
        return OfferingAdminSchema()
458
459
    def populate(self, req, ctx):
460
        super(OfferingNew, self).populate(req, ctx)
1599 by William Grant
Sort subjects and semesters sanely in the offering new form too.
461
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
462
        ctx['semesters'] = req.store.find(Semester).order_by(
463
            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.
464
        ctx['force_subject'] = None
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
465
466
    def populate_state(self, state):
467
        state.existing_offering = None
468
469
    def get_default_data(self, req):
470
        return {}
471
472
    def save_object(self, req, data):
473
        new_offering = Offering()
474
        new_offering.subject = data['subject']
475
        new_offering.semester = data['semester']
476
        new_offering.description = data['description']
477
        new_offering.url = unicode(data['url']) if data['url'] else None
1734 by Matt Giuca
OfferingEdit view: Added Worksheets cutoff field.
478
        new_offering.worksheet_cutoff = data['worksheet_cutoff']
1695.1.4 by William Grant
Expose Offering.show_worksheet_marks in the forms.
479
        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.
480
481
        req.store.add(new_offering)
482
        return new_offering
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
483
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.
484
class SubjectOfferingNew(OfferingNew):
485
    """A form to create an offering for a given subject."""
486
    # Identical to OfferingNew, except it forces the subject to be the subject
487
    # in context
488
    def populate(self, req, ctx):
489
        super(SubjectOfferingNew, self).populate(req, ctx)
490
        ctx['force_subject'] = self.context
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
491
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
492
class OfferingCloneWorksheetsSchema(formencode.Schema):
493
    subject = formencode.All(
494
        SubjectValidator(), formencode.validators.UnicodeString())
495
    semester = formencode.All(
496
        SemesterValidator(), formencode.validators.UnicodeString())
497
498
499
class OfferingCloneWorksheets(BaseFormView):
500
    """A form to clone worksheets from one offering to another."""
501
    template = 'templates/offering-clone-worksheets.html'
502
    tab = 'subjects'
503
504
    def authorize(self, req):
505
        return req.user is not None and req.user.admin
506
507
    @property
508
    def validator(self):
509
        return OfferingCloneWorksheetsSchema()
510
511
    def populate(self, req, ctx):
512
        super(OfferingCloneWorksheets, self).populate(req, ctx)
513
        ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
514
        ctx['semesters'] = req.store.find(Semester).order_by(
515
            Semester.year, Semester.semester)
516
517
    def get_default_data(self, req):
518
        return {}
519
520
    def save_object(self, req, data):
521
        if self.context.worksheets.count() > 0:
522
            raise BadRequest(
523
                "Cannot clone to target with existing worksheets.")
524
        offering = req.store.find(
525
            Offering, subject=data['subject'], semester=data['semester']).one()
526
        if offering is None:
527
            raise BadRequest("No such offering.")
528
        if offering.worksheets.count() == 0:
529
            raise BadRequest("Source offering has no worksheets.")
530
531
        self.context.clone_worksheets(offering)
532
        return self.context
533
534
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
535
class UserValidator(formencode.FancyValidator):
536
    """A FormEncode validator that turns a username into a user.
537
538
    The state must have a 'store' attribute, which is the Storm store
539
    to use."""
540
    def _to_python(self, value, state):
541
        user = User.get_by_login(state.store, value)
542
        if user:
543
            return user
544
        else:
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
545
            raise formencode.Invalid('User does not exist', value, state)
546
547
548
class NoEnrolmentValidator(formencode.FancyValidator):
549
    """A FormEncode validator that ensures absence of an enrolment.
550
551
    The state must have an 'offering' attribute.
552
    """
553
    def _to_python(self, value, state):
554
        if state.offering.get_enrolment(value):
555
            raise formencode.Invalid('User already enrolled', value, state)
556
        return value
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
557
558
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
559
class RoleEnrolmentValidator(formencode.FancyValidator):
560
    """A FormEncode validator that checks permission to enrol users with a
561
    particular role.
562
563
    The state must have an 'offering' attribute.
564
    """
565
    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.
566
        if (("enrol_" + value) not in
567
                state.offering.get_permissions(state.user, state.config)):
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
568
            raise formencode.Invalid('Not allowed to assign users that role',
569
                                     value, state)
570
        return value
571
572
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
573
class EnrolSchema(formencode.Schema):
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
574
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
575
    role = formencode.All(formencode.validators.OneOf(
576
                                ["lecturer", "tutor", "student"]),
577
                          RoleEnrolmentValidator(),
578
                          formencode.validators.UnicodeString())
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
579
580
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
581
class EnrolmentsView(XHTMLView):
582
    """A page which displays all users enrolled in an offering."""
583
    template = 'templates/enrolments.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
584
    tab = 'subjects'
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
585
    permission = 'edit'
1615 by William Grant
Add breadcrumbs for enrolments.
586
    breadcrumb_text = 'Enrolments'
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
587
588
    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.
589
        ctx['req'] = req
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
590
        ctx['offering'] = self.context
1613 by William Grant
Add UI to edit/delete enrolments.
591
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
1614 by William Grant
Only show edit/delete links for enrolments that you can actually touch.
592
        ctx['offering_perms'] = self.context.get_permissions(
593
            req.user, req.config)
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
594
        ctx['EnrolView'] = EnrolView
1613 by William Grant
Add UI to edit/delete enrolments.
595
        ctx['EnrolmentEdit'] = EnrolmentEdit
596
        ctx['EnrolmentDelete'] = EnrolmentDelete
1610 by William Grant
Replace OfferingView's link to EnrolView with one to EnrolmentsView, and link from there to EnrolView.
597
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
598
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
599
class EnrolView(XHTMLView):
600
    """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
601
    template = 'templates/enrol.html'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
602
    tab = 'subjects'
1376 by Matt Giuca
database: More granular permissions on offerings: Added 'enrol' permission.
603
    permission = 'enrol'
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
604
605
    def filter(self, stream, ctx):
606
        return stream | HTMLFormFiller(data=ctx['data'])
607
608
    def populate(self, req, ctx):
609
        if req.method == 'POST':
610
            data = dict(req.get_fieldstorage())
611
            try:
612
                validator = EnrolSchema()
1150 by William Grant
Refuse a +enrol if the user is already enrolled. This stops overwriting
613
                req.offering = self.context # XXX: Getting into state.
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
614
                data = validator.to_python(data, state=req)
1377 by Matt Giuca
database: Added finer-grained enrol permissions on offerings.
615
                self.context.enrol(data['user'], data['role'])
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
616
                req.store.commit()
617
                req.throw_redirect(req.uri)
618
            except formencode.Invalid, e:
619
                errors = e.unpack_errors()
620
        else:
621
            data = {}
622
            errors = {}
623
624
        ctx['data'] = data or {}
625
        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.
626
        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.
627
        ctx['errors'] = errors
1700 by William Grant
Don't hide global form errors on the enrolment form.
628
        # If all of the fields validated, set the global form error.
629
        if isinstance(errors, basestring):
630
            ctx['error_value'] = errors
1149 by William Grant
Allow tutors and lecturers to enrol people in their offerings.
631
1613 by William Grant
Add UI to edit/delete enrolments.
632
633
class EnrolmentEditSchema(formencode.Schema):
634
    role = formencode.All(formencode.validators.OneOf(
635
                                ["lecturer", "tutor", "student"]),
636
                          RoleEnrolmentValidator(),
637
                          formencode.validators.UnicodeString())
638
639
640
class EnrolmentEdit(BaseFormView):
641
    """A form to alter an enrolment's role."""
642
    template = 'templates/enrolment-edit.html'
643
    tab = 'subjects'
644
    permission = 'edit'
645
646
    def populate_state(self, state):
647
        state.offering = self.context.offering
648
649
    def get_default_data(self, req):
650
        return {'role': self.context.role}
651
652
    @property
653
    def validator(self):
654
        return EnrolmentEditSchema()
655
656
    def save_object(self, req, data):
657
        self.context.role = data['role']
658
659
    def get_return_url(self, obj):
660
        return self.req.publisher.generate(
661
            self.context.offering, EnrolmentsView)
662
663
    def populate(self, req, ctx):
664
        super(EnrolmentEdit, self).populate(req, ctx)
665
        ctx['offering_perms'] = self.context.offering.get_permissions(
666
            req.user, req.config)
667
668
669
class EnrolmentDelete(XHTMLView):
670
    """A form to alter an enrolment's role."""
671
    template = 'templates/enrolment-delete.html'
672
    tab = 'subjects'
673
    permission = 'edit'
674
675
    def populate(self, req, ctx):
676
        # If POSTing, delete delete delete.
677
        if req.method == 'POST':
678
            self.context.delete()
679
            req.store.commit()
680
            req.throw_redirect(req.publisher.generate(
681
                self.context.offering, EnrolmentsView))
682
683
        ctx['enrolment'] = self.context
684
685
1165.3.19 by William Grant
Rename SubjectProjectSetView to OfferingProjectsView.
686
class OfferingProjectsView(XHTMLView):
687
    """View the projects for an offering."""
688
    template = 'templates/offering_projects.html'
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
689
    permission = 'edit'
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
690
    tab = 'subjects'
1616 by William Grant
Add a Projects breadcrumb.
691
    breadcrumb_text = 'Projects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
692
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
693
    def populate(self, req, ctx):
694
        self.plugin_styles[Plugin] = ["project.css"]
1361 by William Grant
Remove the last +projectsets hardcoding.
695
        ctx['req'] = req
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
696
        ctx['offering'] = self.context
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
697
        ctx['projectsets'] = []
698
699
        #Open the projectset Fragment, and render it for inclusion
700
        #into the ProjectSets page
701
        set_fragment = os.path.join(os.path.dirname(__file__),
702
                "templates/projectset_fragment.html")
703
        project_fragment = os.path.join(os.path.dirname(__file__),
704
                "templates/project_fragment.html")
705
1718 by Matt Giuca
Projects view: Sort project sets by database ID and projects by deadline, rather than random database ordering. Fixes Launchpad bug #527559.
706
        for projectset in \
707
            self.context.project_sets.order_by(ivle.database.ProjectSet.id):
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
708
            settmpl = self._loader.load(set_fragment)
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
709
            setCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
710
            setCtx['req'] = req
1165.3.30 by William Grant
Clean out the projectset fragment context.
711
            setCtx['projectset'] = projectset
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
712
            setCtx['projects'] = []
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
713
            setCtx['GroupsView'] = GroupsView
1710.1.5 by Matt Giuca
Added new project set edit view. Linked from projects page, project set page.
714
            setCtx['ProjectSetEdit'] = ProjectSetEdit
1710.1.11 by Matt Giuca
Project page: Replace AJAX project creation UI with link to ProjectNew view. Removed project creation UI including JSON API.
715
            setCtx['ProjectNew'] = ProjectNew
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
716
1718 by Matt Giuca
Projects view: Sort project sets by database ID and projects by deadline, rather than random database ordering. Fixes Launchpad bug #527559.
717
            for project in \
718
                projectset.projects.order_by(ivle.database.Project.deadline):
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
719
                projecttmpl = self._loader.load(project_fragment)
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
720
                projectCtx = Context()
1358 by William Grant
Use the publishing framework to generate URLs to projectsets.
721
                projectCtx['req'] = req
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
722
                projectCtx['project'] = project
1710.1.18 by Matt Giuca
Added links to ProjectEdit view, from offering projects page, manage projects page, and individual project pages.
723
                projectCtx['ProjectEdit'] = ProjectEdit
1710.1.20 by Matt Giuca
Fully link the Project Delete view, from all the places the Project Edit view is linked.
724
                projectCtx['ProjectDelete'] = ProjectDelete
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
725
726
                setCtx['projects'].append(
727
                        projecttmpl.generate(projectCtx))
728
729
            ctx['projectsets'].append(settmpl.generate(setCtx))
730
731
732
class ProjectView(XHTMLView):
1165.2.3 by Nick Chadwick
Added a new Admin view, which allows for the administration of projects
733
    """View the submissions for a ProjectSet"""
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
734
    template = "templates/project.html"
1556 by William Grant
Allow tutors to view project submissions.
735
    permission = "view_project_submissions"
1165.3.18 by William Grant
Put the project listing and view in the Subjects tab.
736
    tab = 'subjects'
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
737
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
738
    def build_subversion_url(self, req, submission):
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
739
        princ = submission.assessed.principal
740
1789 by Matt Giuca
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.
741
        return os.path.join(princ.get_svn_url(req.config, req),
1784 by Matt Giuca
ivle.webapp.admin.subject: Abstracted code to generate Subversion URL into the database. User and ProjectGroup objects now have a get_svn_url method.
742
                            submission.path[1:] if
743
                                submission.path.startswith(os.sep) else
744
                                submission.path)
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
745
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
746
    def populate(self, req, ctx):
1165.3.66 by William Grant
Prettify the submissions table.
747
        self.plugin_styles[Plugin] = ["project.css"]
748
1375.1.4 by William Grant
Indicate when there is nobody assigned to a project, and link to the page to fix that.
749
        ctx['req'] = req
1710.1.18 by Matt Giuca
Added links to ProjectEdit view, from offering projects page, manage projects page, and individual project pages.
750
        ctx['permissions'] = self.context.get_permissions(req.user,req.config)
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['GroupsView'] = GroupsView
752
        ctx['EnrolView'] = EnrolView
1719 by Matt Giuca
Project page: Added URL and deadline, so this page now shows all fields of the project. Fixes Launchpad bug #527560.
753
        ctx['format_datetime'] = ivle.date.make_date_nice
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
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
756
        ctx['project'] = self.context
1165.3.61 by William Grant
Provide a Subversion command to grab each submission.
757
        ctx['user'] = req.user
1710.1.18 by Matt Giuca
Added links to ProjectEdit view, from offering projects page, manage projects page, and individual project pages.
758
        ctx['ProjectEdit'] = ProjectEdit
1710.1.20 by Matt Giuca
Fully link the Project Delete view, from all the places the Project Edit view is linked.
759
        ctx['ProjectDelete'] = ProjectDelete
1165.3.2 by Nick Chadwick
Created a new view for IVLE, allowing lecturers and tutors to
760
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
761
class ProjectUniquenessValidator(formencode.FancyValidator):
762
    """A FormEncode validator that checks that a project short_name is unique
763
    in a given offering.
764
765
    The project referenced by state.existing_project is permitted to
766
    hold that short_name. If any other project holds it, the input is rejected.
767
    """
768
    def _to_python(self, value, state):
1710.1.15 by Matt Giuca
subject.py: ProjectUniquenessValidator now allows a project to have the same shortname as the one it is replacing (allowing you to edit a project without changing its short name, once edit is implemented).
769
        if (state.store.find(
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
770
            Project,
771
            Project.short_name == unicode(value),
772
            Project.project_set_id == ProjectSet.id,
1710.1.15 by Matt Giuca
subject.py: ProjectUniquenessValidator now allows a project to have the same shortname as the one it is replacing (allowing you to edit a project without changing its short name, once edit is implemented).
773
            ProjectSet.offering == state.offering).one() not in
774
            (None, state.existing_project)):
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
775
            raise formencode.Invalid(
776
                "A project with that URL name already exists in this offering."
777
                , value, state)
778
        return value
779
780
class ProjectSchema(formencode.Schema):
781
    name = formencode.validators.UnicodeString(not_empty=True)
782
    short_name = formencode.All(
783
        URLNameValidator(not_empty=True),
784
        ProjectUniquenessValidator())
785
    deadline = DateTimeValidator(not_empty=True)
786
    url = formencode.validators.URL(if_missing=None, not_empty=False)
787
    synopsis = formencode.validators.UnicodeString(not_empty=True)
788
1710.1.17 by Matt Giuca
Added project edit page (based on project new view). Currently unlinked.
789
class ProjectEdit(BaseFormView):
790
    """A form to edit a project."""
791
    template = 'templates/project-edit.html'
792
    tab = 'subjects'
793
    permission = 'edit'
794
795
    @property
796
    def validator(self):
797
        return ProjectSchema()
798
799
    def populate(self, req, ctx):
800
        super(ProjectEdit, self).populate(req, ctx)
801
        ctx['projectset'] = self.context.project_set
802
803
    def populate_state(self, state):
804
        state.offering = self.context.project_set.offering
805
        state.existing_project = self.context
806
807
    def get_default_data(self, req):
808
        return {
809
            'name':         self.context.name,
810
            'short_name':   self.context.short_name,
811
            'deadline':     self.context.deadline,
812
            'url':          self.context.url,
813
            'synopsis':     self.context.synopsis,
814
            }
815
816
    def save_object(self, req, data):
817
        self.context.name = data['name']
818
        self.context.short_name = data['short_name']
819
        self.context.deadline = data['deadline']
820
        self.context.url = unicode(data['url']) if data['url'] else None
821
        self.context.synopsis = data['synopsis']
822
        return self.context
823
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
824
class ProjectNew(BaseFormView):
825
    """A form to create a new project."""
826
    template = 'templates/project-new.html'
827
    tab = 'subjects'
828
    permission = 'edit'
829
830
    @property
831
    def validator(self):
832
        return ProjectSchema()
833
834
    def populate(self, req, ctx):
835
        super(ProjectNew, self).populate(req, ctx)
1710.1.16 by Matt Giuca
project-form no longer assumes the type of context; pass an extra projectset value.
836
        ctx['projectset'] = self.context
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
837
838
    def populate_state(self, state):
839
        state.offering = self.context.offering
840
        state.existing_project = None
841
842
    def get_default_data(self, req):
843
        return {}
844
845
    def save_object(self, req, data):
846
        new_project = Project()
847
        new_project.project_set = self.context
848
        new_project.name = data['name']
849
        new_project.short_name = data['short_name']
850
        new_project.deadline = data['deadline']
1710.1.10 by Matt Giuca
ProjectNew view: Convert URLs to Unicode. This would previously break if a URL was entered.
851
        new_project.url = unicode(data['url']) if data['url'] else None
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
852
        new_project.synopsis = data['synopsis']
853
        req.store.add(new_project)
854
        return new_project
855
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
856
class ProjectDelete(XHTMLView):
857
    """A form to delete a project."""
858
    template = 'templates/project-delete.html'
859
    tab = 'subjects'
860
    permission = 'edit'
861
862
    def populate(self, req, ctx):
863
        # If post, delete the project, or display a message explaining that
864
        # the project cannot be deleted
865
        if self.context.can_delete:
866
            if req.method == 'POST':
867
                self.context.delete()
868
                self.template = 'templates/project-deleted.html'
869
        else:
870
            # Can't delete
871
            self.template = 'templates/project-undeletable.html'
872
873
        # If get and can delete, display a delete confirmation page
874
875
        # Variables for the template
876
        ctx['req'] = req
877
        ctx['project'] = self.context
878
        ctx['OfferingProjectsView'] = OfferingProjectsView
879
1710.1.1 by Matt Giuca
Added new view for adding a new project set (offering/+projects/+new-set). This replaces the AJAX UI. The 'Add a new project set' link now links to the static 'new project set' page.
880
class ProjectSetSchema(formencode.Schema):
881
    group_size = formencode.validators.Int(if_missing=None, not_empty=False)
882
1710.1.5 by Matt Giuca
Added new project set edit view. Linked from projects page, project set page.
883
class ProjectSetEdit(BaseFormView):
884
    """A form to edit a project set."""
885
    template = 'templates/projectset-edit.html'
886
    tab = 'subjects'
887
    permission = 'edit'
888
889
    @property
890
    def validator(self):
891
        return ProjectSetSchema()
892
893
    def populate(self, req, ctx):
894
        super(ProjectSetEdit, self).populate(req, ctx)
895
896
    def get_default_data(self, req):
897
        return {
898
            'group_size': self.context.max_students_per_group,
899
            }
900
901
    def save_object(self, req, data):
902
        self.context.max_students_per_group = data['group_size']
903
        return self.context
904
1710.1.1 by Matt Giuca
Added new view for adding a new project set (offering/+projects/+new-set). This replaces the AJAX UI. The 'Add a new project set' link now links to the static 'new project set' page.
905
class ProjectSetNew(BaseFormView):
906
    """A form to create a new project set."""
907
    template = 'templates/projectset-new.html'
908
    tab = 'subjects'
909
    permission = 'edit'
910
    breadcrumb_text = "Projects"
911
912
    @property
913
    def validator(self):
914
        return ProjectSetSchema()
915
916
    def populate(self, req, ctx):
917
        super(ProjectSetNew, self).populate(req, ctx)
918
919
    def get_default_data(self, req):
920
        return {}
921
922
    def save_object(self, req, data):
923
        new_set = ProjectSet()
924
        new_set.offering = self.context
925
        new_set.max_students_per_group = data['group_size']
926
        req.store.add(new_set)
927
        return new_set
928
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
929
class Plugin(ViewPlugin, MediaPlugin):
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
930
    forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
1613 by William Grant
Add UI to edit/delete enrolments.
931
                      offering_to_project, offering_to_projectset,
932
                      offering_to_enrolment)
1592 by William Grant
Add routes for Semester. We'll need them for the admin UI.
933
    reverse_routes = (
1613 by William Grant
Add UI to edit/delete enrolments.
934
        subject_url, semester_url, offering_url, projectset_url, project_url,
935
        enrolment_url)
1294.2.52 by William Grant
Port subject-related views to object traversal.
936
937
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
1596 by William Grant
Split subject/semester management out onto a separate page, and link to SemesterEdit.
938
             (ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
939
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
1537 by William Grant
Add offering creation UI, and allow admins to change the subject or semester of existing offerings.
940
             (ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
1594 by William Grant
Add semester edit UI.
941
             (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.
942
             (Subject, '+index', SubjectView),
1532 by William Grant
Add subject creation/editing UI. Not linked just yet.
943
             (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.
944
             (Subject, '+new-offering', SubjectOfferingNew),
1594 by William Grant
Add semester edit UI.
945
             (Semester, '+edit', SemesterEdit),
1442.1.2 by William Grant
Add basic (ie. pretty much empty) offering index.
946
             (Offering, '+index', OfferingView),
1451.1.5 by William Grant
Add an OfferingEdit view, for setting the description and URL.
947
             (Offering, '+edit', OfferingEdit),
1603 by William Grant
Add UI to clone worksheets between offerings -- replacing ivle-cloneworksheets.
948
             (Offering, '+clone-worksheets', OfferingCloneWorksheets),
1365 by Matt Giuca
Added a new view under Offering/+enrolments to display all staff and students in an offering.
949
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
1294.2.52 by William Grant
Port subject-related views to object traversal.
950
             (Offering, ('+enrolments', '+new'), EnrolView),
1613 by William Grant
Add UI to edit/delete enrolments.
951
             (Enrolment, '+edit', EnrolmentEdit),
952
             (Enrolment, '+delete', EnrolmentDelete),
1294.2.52 by William Grant
Port subject-related views to object traversal.
953
             (Offering, ('+projects', '+index'), OfferingProjectsView),
1710.1.1 by Matt Giuca
Added new view for adding a new project set (offering/+projects/+new-set). This replaces the AJAX UI. The 'Add a new project set' link now links to the static 'new project set' page.
954
             (Offering, ('+projects', '+new-set'), ProjectSetNew),
1710.1.5 by Matt Giuca
Added new project set edit view. Linked from projects page, project set page.
955
             (ProjectSet, '+edit', ProjectSetEdit),
1710.1.8 by Matt Giuca
Added 'New Project' non-AJAX UI to replace old AJAX one. Currently not linked anywhere; AJAX one still works.
956
             (ProjectSet, '+new', ProjectNew),
1294.2.52 by William Grant
Port subject-related views to object traversal.
957
             (Project, '+index', ProjectView),
1710.1.17 by Matt Giuca
Added project edit page (based on project new view). Currently unlinked.
958
             (Project, '+edit', ProjectEdit),
1710.1.19 by Matt Giuca
Added project delete view, and ability to delete projects in the database. Currently unlinked.
959
             (Project, '+delete', ProjectDelete),
1294.2.52 by William Grant
Port subject-related views to object traversal.
960
             ]
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
961
1294.2.94 by William Grant
Add a SubjectBreadcrumb.
962
    breadcrumbs = {Subject: SubjectBreadcrumb,
963
                   Offering: OfferingBreadcrumb,
1294.2.96 by William Grant
Add a UserBreadcrumb.
964
                   User: UserBreadcrumb,
1294.2.98 by William Grant
Add a ProjectBreadcrumb.
965
                   Project: ProjectBreadcrumb,
1615 by William Grant
Add breadcrumbs for enrolments.
966
                   Enrolment: EnrolmentBreadcrumb,
1294.2.89 by William Grant
Add an Offering breadcrumb.
967
                   }
968
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
969
    tabs = [
1118 by matt.giuca
Rewrote tooltips for the four tabs visible by default.
970
        ('subjects', 'Subjects',
971
         'View subject content and complete worksheets',
972
         'subjects.png', 'subjects', 5)
1099.1.115 by William Grant
Add tabs to the new framework. Move the app icons into the apps themselves.
973
    ]
974
975
    media = 'subject-media'