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

« back to all changes in this revision

Viewing changes to ivle/webapp/admin/subject.py

  • Committer: William Grant
  • Date: 2010-02-11 12:17:37 UTC
  • Revision ID: grantw@unimelb.edu.au-20100211121737-1zsmpp8i8rbyliku
Add a subject listing with new/edit icons.

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
import urlparse
29
29
import cgi
30
30
 
31
 
from storm.locals import Desc
 
31
from storm.locals import Desc, Store
32
32
import genshi
33
33
from genshi.filters import HTMLFormFiller
34
34
from genshi.template import Context, TemplateLoader
35
35
import formencode
 
36
import formencode.validators
36
37
 
37
38
from ivle.webapp.base.xhtml import XHTMLView
38
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
39
 
from ivle.webapp.errors import NotFound
 
40
from ivle.webapp import ApplicationRoot
40
41
 
41
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
42
43
                          ProjectSet, Project, ProjectSubmission
43
44
from ivle import util
44
45
import ivle.date
45
46
 
46
 
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
47
 
                                             ProjectRESTView
 
47
from ivle.webapp.admin.projectservice import ProjectSetRESTView
48
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
49
 
 
 
49
from ivle.webapp.admin.publishing import (root_to_subject,
 
50
            subject_to_offering, offering_to_projectset, offering_to_project,
 
51
            subject_url, offering_url, projectset_url, project_url)
 
52
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
 
53
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
54
from ivle.webapp.core import Plugin as CorePlugin
 
55
from ivle.webapp.groups import GroupsView
 
56
from ivle.webapp.media import media_url
 
57
from ivle.webapp.tutorial import Plugin as TutorialPlugin
50
58
 
51
59
class SubjectsView(XHTMLView):
52
60
    '''The view of the list of subjects.'''
57
65
        return req.user is not None
58
66
 
59
67
    def populate(self, req, ctx):
 
68
        ctx['req'] = req
60
69
        ctx['user'] = req.user
61
70
        ctx['semesters'] = []
 
71
        ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
 
72
        ctx['SubjectEdit'] = SubjectEdit
 
73
 
62
74
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
63
75
                                                     Desc(Semester.semester)):
64
 
            enrolments = semester.enrolments.find(user=req.user)
65
 
            if enrolments.count():
66
 
                ctx['semesters'].append((semester, enrolments))
 
76
            if req.user.admin:
 
77
                # For admins, show all subjects in the system
 
78
                offerings = list(semester.offerings.find())
 
79
            else:
 
80
                offerings = [enrolment.offering for enrolment in
 
81
                                    semester.enrolments.find(user=req.user)]
 
82
            if len(offerings):
 
83
                ctx['semesters'].append((semester, offerings))
 
84
 
 
85
        # Admins get a separate list of subjects so they can add/edit.
 
86
        if req.user.admin:
 
87
            ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
 
88
 
 
89
 
 
90
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
 
91
    """A FormEncode validator that checks that a subject name is unused.
 
92
 
 
93
    The subject referenced by state.existing_subject is permitted
 
94
    to hold that name. If any other object holds it, the input is rejected.
 
95
    """
 
96
    def __init__(self, matching=None):
 
97
        self.matching = matching
 
98
 
 
99
    def _to_python(self, value, state):
 
100
        if (state.store.find(
 
101
                Subject, short_name=value).one() not in
 
102
                (None, state.existing_subject)):
 
103
            raise formencode.Invalid(
 
104
                'Short name already taken', value, state)
 
105
        return value
 
106
 
 
107
 
 
108
class SubjectSchema(formencode.Schema):
 
109
    short_name = formencode.All(
 
110
        SubjectShortNameUniquenessValidator(),
 
111
        formencode.validators.UnicodeString(not_empty=True))
 
112
    name = formencode.validators.UnicodeString(not_empty=True)
 
113
    code = formencode.validators.UnicodeString(not_empty=True)
 
114
 
 
115
 
 
116
class SubjectFormView(XHTMLView):
 
117
    """An abstract form to add or edit a subject."""
 
118
    tab = 'subjects'
 
119
 
 
120
    def authorize(self, req):
 
121
        return req.user is not None and req.user.admin
 
122
 
 
123
    def filter(self, stream, ctx):
 
124
        return stream | HTMLFormFiller(data=ctx['data'])
 
125
 
 
126
    def populate_state(self, state):
 
127
        state.existing_subject = None
 
128
 
 
129
    def populate(self, req, ctx):
 
130
        if req.method == 'POST':
 
131
            data = dict(req.get_fieldstorage())
 
132
            try:
 
133
                validator = SubjectSchema()
 
134
                self.populate_state(req)
 
135
                data = validator.to_python(data, state=req)
 
136
 
 
137
                subject = self.update_subject_object(req, data)
 
138
 
 
139
                req.store.commit()
 
140
                req.throw_redirect(req.publisher.generate(subject))
 
141
            except formencode.Invalid, e:
 
142
                errors = e.unpack_errors()
 
143
        else:
 
144
            data = self.get_default_data(req)
 
145
            errors = {}
 
146
 
 
147
        if errors:
 
148
            req.store.rollback()
 
149
 
 
150
        ctx['context'] = self.context
 
151
        ctx['data'] = data or {}
 
152
        ctx['errors'] = errors
 
153
 
 
154
 
 
155
class SubjectNew(SubjectFormView):
 
156
    """A form to create a subject."""
 
157
    template = 'templates/subject-new.html'
 
158
 
 
159
    def populate_state(self, state):
 
160
        state.existing_subject = self.context
 
161
 
 
162
    def get_default_data(self, req):
 
163
        return {}
 
164
 
 
165
    def update_subject_object(self, req, data):
 
166
        new_subject = Subject()
 
167
        new_subject.short_name = data['short_name']
 
168
        new_subject.name = data['name']
 
169
        new_subject.code = data['code']
 
170
 
 
171
        req.store.add(new_subject)
 
172
        return new_subject
 
173
 
 
174
 
 
175
class SubjectEdit(SubjectFormView):
 
176
    """A form to edit a subject."""
 
177
    template = 'templates/subject-edit.html'
 
178
 
 
179
    def populate_state(self, state):
 
180
        state.existing_subject = self.context
 
181
 
 
182
    def get_default_data(self, req):
 
183
        return {
 
184
            'short_name': self.context.short_name,
 
185
            'name': self.context.name,
 
186
            'code': self.context.code,
 
187
            }
 
188
 
 
189
    def update_subject_object(self, req, data):
 
190
        self.context.short_name = data['short_name']
 
191
        self.context.name = data['name']
 
192
        self.context.code = data['code']
 
193
 
 
194
        return self.context
 
195
 
 
196
 
 
197
class OfferingView(XHTMLView):
 
198
    """The home page of an offering."""
 
199
    template = 'templates/offering.html'
 
200
    tab = 'subjects'
 
201
    permission = 'view'
 
202
 
 
203
    def populate(self, req, ctx):
 
204
        # Need the worksheet result styles.
 
205
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
206
        ctx['context'] = self.context
 
207
        ctx['req'] = req
 
208
        ctx['permissions'] = self.context.get_permissions(req.user)
 
209
        ctx['format_submission_principal'] = util.format_submission_principal
 
210
        ctx['format_datetime'] = ivle.date.make_date_nice
 
211
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
212
        ctx['OfferingEdit'] = OfferingEdit
 
213
 
 
214
        # As we go, calculate the total score for this subject
 
215
        # (Assessable worksheets only, mandatory problems only)
 
216
 
 
217
        ctx['worksheets'], problems_total, problems_done = (
 
218
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
219
                req.store, req.user, self.context))
 
220
 
 
221
        ctx['exercises_total'] = problems_total
 
222
        ctx['exercises_done'] = problems_done
 
223
        if problems_total > 0:
 
224
            if problems_done >= problems_total:
 
225
                ctx['worksheets_complete_class'] = "complete"
 
226
            elif problems_done > 0:
 
227
                ctx['worksheets_complete_class'] = "semicomplete"
 
228
            else:
 
229
                ctx['worksheets_complete_class'] = "incomplete"
 
230
            # Calculate the final percentage and mark for the subject
 
231
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
232
             ctx['worksheet_max_mark']) = (
 
233
                ivle.worksheet.utils.calculate_mark(
 
234
                    problems_done, problems_total))
 
235
 
 
236
 
 
237
class OfferingSchema(formencode.Schema):
 
238
    description = formencode.validators.UnicodeString(
 
239
        if_missing=None, not_empty=False)
 
240
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
241
 
 
242
 
 
243
class OfferingEdit(XHTMLView):
 
244
    """A form to edit an offering's details."""
 
245
    template = 'templates/offering-edit.html'
 
246
    tab = 'subjects'
 
247
    permission = 'edit'
 
248
 
 
249
    def filter(self, stream, ctx):
 
250
        return stream | HTMLFormFiller(data=ctx['data'])
 
251
 
 
252
    def populate(self, req, ctx):
 
253
        if req.method == 'POST':
 
254
            data = dict(req.get_fieldstorage())
 
255
            try:
 
256
                validator = OfferingSchema()
 
257
                data = validator.to_python(data, state=req)
 
258
 
 
259
                self.context.url = unicode(data['url']) if data['url'] else None
 
260
                self.context.description = data['description']
 
261
                req.store.commit()
 
262
                req.throw_redirect(req.publisher.generate(self.context))
 
263
            except formencode.Invalid, e:
 
264
                errors = e.unpack_errors()
 
265
        else:
 
266
            data = {
 
267
                'url': self.context.url,
 
268
                'description': self.context.description,
 
269
            }
 
270
            errors = {}
 
271
 
 
272
        ctx['data'] = data or {}
 
273
        ctx['context'] = self.context
 
274
        ctx['errors'] = errors
67
275
 
68
276
 
69
277
class UserValidator(formencode.FancyValidator):
90
298
        return value
91
299
 
92
300
 
 
301
class RoleEnrolmentValidator(formencode.FancyValidator):
 
302
    """A FormEncode validator that checks permission to enrol users with a
 
303
    particular role.
 
304
 
 
305
    The state must have an 'offering' attribute.
 
306
    """
 
307
    def _to_python(self, value, state):
 
308
        if ("enrol_" + value) not in state.offering.get_permissions(state.user):
 
309
            raise formencode.Invalid('Not allowed to assign users that role',
 
310
                                     value, state)
 
311
        return value
 
312
 
 
313
 
93
314
class EnrolSchema(formencode.Schema):
94
315
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
95
 
 
 
316
    role = formencode.All(formencode.validators.OneOf(
 
317
                                ["lecturer", "tutor", "student"]),
 
318
                          RoleEnrolmentValidator(),
 
319
                          formencode.validators.UnicodeString())
 
320
 
 
321
 
 
322
class EnrolmentsView(XHTMLView):
 
323
    """A page which displays all users enrolled in an offering."""
 
324
    template = 'templates/enrolments.html'
 
325
    tab = 'subjects'
 
326
    permission = 'edit'
 
327
 
 
328
    def populate(self, req, ctx):
 
329
        ctx['offering'] = self.context
96
330
 
97
331
class EnrolView(XHTMLView):
98
332
    """A form to enrol a user in an offering."""
99
333
    template = 'templates/enrol.html'
100
334
    tab = 'subjects'
101
 
    permission = 'edit'
102
 
 
103
 
    def __init__(self, req, subject, year, semester):
104
 
        """Find the given offering by subject, year and semester."""
105
 
        self.context = req.store.find(Offering,
106
 
            Offering.subject_id == Subject.id,
107
 
            Subject.short_name == subject,
108
 
            Offering.semester_id == Semester.id,
109
 
            Semester.year == year,
110
 
            Semester.semester == semester).one()
111
 
 
112
 
        if not self.context:
113
 
            raise NotFound()
 
335
    permission = 'enrol'
114
336
 
115
337
    def filter(self, stream, ctx):
116
338
        return stream | HTMLFormFiller(data=ctx['data'])
122
344
                validator = EnrolSchema()
123
345
                req.offering = self.context # XXX: Getting into state.
124
346
                data = validator.to_python(data, state=req)
125
 
                self.context.enrol(data['user'])
 
347
                self.context.enrol(data['user'], data['role'])
126
348
                req.store.commit()
127
349
                req.throw_redirect(req.uri)
128
350
            except formencode.Invalid, e:
133
355
 
134
356
        ctx['data'] = data or {}
135
357
        ctx['offering'] = self.context
 
358
        ctx['roles_auth'] = self.context.get_permissions(req.user)
136
359
        ctx['errors'] = errors
137
360
 
138
361
class OfferingProjectsView(XHTMLView):
140
363
    template = 'templates/offering_projects.html'
141
364
    permission = 'edit'
142
365
    tab = 'subjects'
143
 
    
144
 
    def __init__(self, req, subject, year, semester):
145
 
        self.context = req.store.find(Offering,
146
 
            Offering.subject_id == Subject.id,
147
 
            Subject.short_name == subject,
148
 
            Offering.semester_id == Semester.id,
149
 
            Semester.year == year,
150
 
            Semester.semester == semester).one()
151
 
 
152
 
        if not self.context:
153
 
            raise NotFound()
154
 
 
155
 
    def project_url(self, projectset, project):
156
 
        return "/subjects/%s/%s/%s/+projects/%s" % (
157
 
                    self.context.subject.short_name,
158
 
                    self.context.semester.year,
159
 
                    self.context.semester.semester,
160
 
                    project.short_name
161
 
                    )
162
 
 
163
 
    def new_project_url(self, projectset):
164
 
        return "/api/subjects/" + self.context.subject.short_name + "/" +\
165
 
                self.context.semester.year + "/" + \
166
 
                self.context.semester.semester + "/+projectsets/" +\
167
 
                str(projectset.id) + "/+projects/+new"
168
 
    
 
366
 
169
367
    def populate(self, req, ctx):
170
368
        self.plugin_styles[Plugin] = ["project.css"]
171
369
        self.plugin_scripts[Plugin] = ["project.js"]
 
370
        ctx['req'] = req
172
371
        ctx['offering'] = self.context
173
372
        ctx['projectsets'] = []
 
373
        ctx['OfferingRESTView'] = OfferingRESTView
174
374
 
175
375
        #Open the projectset Fragment, and render it for inclusion
176
376
        #into the ProjectSets page
185
385
        for projectset in self.context.project_sets:
186
386
            settmpl = loader.load(set_fragment)
187
387
            setCtx = Context()
 
388
            setCtx['req'] = req
188
389
            setCtx['projectset'] = projectset
189
 
            setCtx['new_project_url'] = self.new_project_url(projectset)
190
390
            setCtx['projects'] = []
 
391
            setCtx['GroupsView'] = GroupsView
 
392
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
191
393
 
192
394
            for project in projectset.projects:
193
395
                projecttmpl = loader.load(project_fragment)
194
396
                projectCtx = Context()
 
397
                projectCtx['req'] = req
195
398
                projectCtx['project'] = project
196
 
                projectCtx['project_url'] = self.project_url(projectset, project)
197
399
 
198
400
                setCtx['projects'].append(
199
401
                        projecttmpl.generate(projectCtx))
207
409
    permission = "edit"
208
410
    tab = 'subjects'
209
411
 
210
 
    def __init__(self, req, subject, year, semester, project):
211
 
        self.context = req.store.find(Project,
212
 
                Project.short_name == project,
213
 
                Project.project_set_id == ProjectSet.id,
214
 
                ProjectSet.offering_id == Offering.id,
215
 
                Offering.semester_id == Semester.id,
216
 
                Semester.year == year,
217
 
                Semester.semester == semester,
218
 
                Offering.subject_id == Subject.id,
219
 
                Subject.short_name == subject).one()
220
 
        if self.context is None:
221
 
            raise NotFound()
222
 
 
223
412
    def build_subversion_url(self, svnroot, submission):
224
413
        princ = submission.assessed.principal
225
414
 
241
430
    def populate(self, req, ctx):
242
431
        self.plugin_styles[Plugin] = ["project.css"]
243
432
 
 
433
        ctx['req'] = req
 
434
        ctx['GroupsView'] = GroupsView
 
435
        ctx['EnrolView'] = EnrolView
244
436
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
245
437
        ctx['build_subversion_url'] = self.build_subversion_url
246
438
        ctx['svn_addr'] = req.config['urls']['svn_addr']
248
440
        ctx['user'] = req.user
249
441
 
250
442
class Plugin(ViewPlugin, MediaPlugin):
251
 
    urls = [
252
 
        ('subjects/', SubjectsView),
253
 
        ('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
254
 
        ('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
255
 
        ('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
256
 
        #API Views
257
 
        ('api/subjects/:subject/:year/:semester/+projectsets/+new',
258
 
            OfferingRESTView),
259
 
        ('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
260
 
            ProjectSetRESTView),
261
 
        ('api/subjects/:subject/:year/:semester/+projects/:project', 
262
 
            ProjectRESTView),
263
 
 
264
 
    ]
 
443
    forward_routes = (root_to_subject, subject_to_offering,
 
444
                      offering_to_project, offering_to_projectset)
 
445
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
446
 
 
447
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
448
             (ApplicationRoot, ('subjects', '+new'), SubjectNew),
 
449
             (Subject, '+edit', SubjectEdit),
 
450
             (Offering, '+index', OfferingView),
 
451
             (Offering, '+edit', OfferingEdit),
 
452
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
453
             (Offering, ('+enrolments', '+new'), EnrolView),
 
454
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
455
             (Project, '+index', ProjectView),
 
456
 
 
457
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
458
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
459
             ]
 
460
 
 
461
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
462
                   Offering: OfferingBreadcrumb,
 
463
                   User: UserBreadcrumb,
 
464
                   Project: ProjectBreadcrumb,
 
465
                   }
265
466
 
266
467
    tabs = [
267
468
        ('subjects', 'Subjects',