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