~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-01-28 22:37:23 UTC
  • mto: This revision was merged to the branch mainline in revision 1454.
  • Revision ID: me@williamgrant.id.au-20100128223723-dbg4zb37xsv3ahwd
Add a 'Change details' link on the offering index, pointing to +edit.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
25
import os
 
26
import os.path
 
27
import urllib
 
28
import urlparse
 
29
import cgi
 
30
 
 
31
from storm.locals import Desc, Store
 
32
import genshi
 
33
from genshi.filters import HTMLFormFiller
 
34
from genshi.template import Context, TemplateLoader
 
35
import formencode
 
36
 
 
37
from ivle.webapp.base.xhtml import XHTMLView
 
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
39
from ivle.webapp import ApplicationRoot
 
40
 
 
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
42
                          ProjectSet, Project, ProjectSubmission
 
43
from ivle import util
 
44
import ivle.date
 
45
 
 
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
 
47
from ivle.webapp.admin.offeringservice import OfferingRESTView
 
48
from ivle.webapp.admin.publishing import (root_to_subject,
 
49
            subject_to_offering, offering_to_projectset, offering_to_project,
 
50
            subject_url, offering_url, projectset_url, project_url)
 
51
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
 
52
            OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
 
53
from ivle.webapp.groups import GroupsView
 
54
from ivle.webapp.tutorial import Plugin as TutorialPlugin
 
55
 
 
56
class SubjectsView(XHTMLView):
 
57
    '''The view of the list of subjects.'''
 
58
    template = 'templates/subjects.html'
 
59
    tab = 'subjects'
 
60
 
 
61
    def authorize(self, req):
 
62
        return req.user is not None
 
63
 
 
64
    def populate(self, req, ctx):
 
65
        ctx['user'] = req.user
 
66
        ctx['semesters'] = []
 
67
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
 
68
                                                     Desc(Semester.semester)):
 
69
            if req.user.admin:
 
70
                # For admins, show all subjects in the system
 
71
                offerings = list(semester.offerings.find())
 
72
            else:
 
73
                offerings = [enrolment.offering for enrolment in
 
74
                                    semester.enrolments.find(user=req.user)]
 
75
            if len(offerings):
 
76
                ctx['semesters'].append((semester, offerings))
 
77
 
 
78
 
 
79
def format_submission_principal(user, principal):
 
80
    """Render a list of users to fit in the offering project listing.
 
81
 
 
82
    Given a user and a list of submitters, returns 'solo' if the
 
83
    only submitter is the user, or a string of the form
 
84
    'with A, B and C' if there are any other submitters.
 
85
 
 
86
    If submitters is None, we assume that the list of members could
 
87
    not be determined, so we just return 'group'.
 
88
    """
 
89
    if principal is None:
 
90
        return 'group'
 
91
 
 
92
    if principal is user:
 
93
        return 'solo'
 
94
 
 
95
    display_names = sorted(
 
96
        member.display_name for member in principal.members
 
97
        if member is not user)
 
98
 
 
99
    if len(display_names) == 0:
 
100
        return 'solo (%s)' % principal.name
 
101
    elif len(display_names) == 1:
 
102
        return 'with %s (%s)' % (display_names[0], principal.name)
 
103
    elif len(display_names) > 5:
 
104
        return 'with %d others (%s)' % (len(display_names), principal.name)
 
105
    else:
 
106
        return 'with %s and %s (%s)' % (', '.join(display_names[:-1]),
 
107
                                        display_names[-1], principal.name)
 
108
 
 
109
 
 
110
class OfferingView(XHTMLView):
 
111
    """The home page of an offering."""
 
112
    template = 'templates/offering.html'
 
113
    tab = 'subjects'
 
114
    permission = 'view'
 
115
 
 
116
    def populate(self, req, ctx):
 
117
        # Need the worksheet result styles.
 
118
        self.plugin_styles[TutorialPlugin] = ['tutorial.css']
 
119
        ctx['context'] = self.context
 
120
        ctx['req'] = req
 
121
        ctx['permissions'] = self.context.get_permissions(req.user)
 
122
        ctx['format_submission_principal'] = format_submission_principal
 
123
        ctx['format_datetime'] = ivle.date.make_date_nice
 
124
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
125
        ctx['OfferingEdit'] = OfferingEdit
 
126
 
 
127
        # As we go, calculate the total score for this subject
 
128
        # (Assessable worksheets only, mandatory problems only)
 
129
 
 
130
        ctx['worksheets'], problems_total, problems_done = (
 
131
            ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
 
132
                req.store, req.user, self.context))
 
133
 
 
134
        ctx['exercises_total'] = problems_total
 
135
        ctx['exercises_done'] = problems_done
 
136
        if problems_total > 0:
 
137
            if problems_done >= problems_total:
 
138
                ctx['worksheets_complete_class'] = "complete"
 
139
            elif problems_done > 0:
 
140
                ctx['worksheets_complete_class'] = "semicomplete"
 
141
            else:
 
142
                ctx['worksheets_complete_class'] = "incomplete"
 
143
            # Calculate the final percentage and mark for the subject
 
144
            (ctx['exercises_pct'], ctx['worksheet_mark'],
 
145
             ctx['worksheet_max_mark']) = (
 
146
                ivle.worksheet.utils.calculate_mark(
 
147
                    problems_done, problems_total))
 
148
 
 
149
 
 
150
class OfferingSchema(formencode.Schema):
 
151
    description = formencode.validators.UnicodeString()
 
152
    url = formencode.validators.URL()
 
153
 
 
154
 
 
155
class OfferingEdit(XHTMLView):
 
156
    """A form to edit an offering's details."""
 
157
    template = 'templates/offering-edit.html'
 
158
    permission = 'edit'
 
159
 
 
160
    def filter(self, stream, ctx):
 
161
        return stream | HTMLFormFiller(data=ctx['data'])
 
162
 
 
163
    def populate(self, req, ctx):
 
164
        if req.method == 'POST':
 
165
            data = dict(req.get_fieldstorage())
 
166
            try:
 
167
                validator = OfferingSchema()
 
168
                data = validator.to_python(data, state=req)
 
169
 
 
170
                self.context.url = unicode(data['url'])
 
171
                self.context.description = data['description']
 
172
                req.store.commit()
 
173
                req.throw_redirect(req.publisher.generate(self.context))
 
174
            except formencode.Invalid, e:
 
175
                errors = e.unpack_errors()
 
176
        else:
 
177
            data = {
 
178
                'url': self.context.url,
 
179
                'description': self.context.description,
 
180
            }
 
181
            errors = {}
 
182
 
 
183
        ctx['data'] = data or {}
 
184
        ctx['context'] = self.context
 
185
        ctx['errors'] = errors
 
186
 
 
187
 
 
188
class UserValidator(formencode.FancyValidator):
 
189
    """A FormEncode validator that turns a username into a user.
 
190
 
 
191
    The state must have a 'store' attribute, which is the Storm store
 
192
    to use."""
 
193
    def _to_python(self, value, state):
 
194
        user = User.get_by_login(state.store, value)
 
195
        if user:
 
196
            return user
 
197
        else:
 
198
            raise formencode.Invalid('User does not exist', value, state)
 
199
 
 
200
 
 
201
class NoEnrolmentValidator(formencode.FancyValidator):
 
202
    """A FormEncode validator that ensures absence of an enrolment.
 
203
 
 
204
    The state must have an 'offering' attribute.
 
205
    """
 
206
    def _to_python(self, value, state):
 
207
        if state.offering.get_enrolment(value):
 
208
            raise formencode.Invalid('User already enrolled', value, state)
 
209
        return value
 
210
 
 
211
 
 
212
class RoleEnrolmentValidator(formencode.FancyValidator):
 
213
    """A FormEncode validator that checks permission to enrol users with a
 
214
    particular role.
 
215
 
 
216
    The state must have an 'offering' attribute.
 
217
    """
 
218
    def _to_python(self, value, state):
 
219
        if ("enrol_" + value) not in state.offering.get_permissions(state.user):
 
220
            raise formencode.Invalid('Not allowed to assign users that role',
 
221
                                     value, state)
 
222
        return value
 
223
 
 
224
 
 
225
class EnrolSchema(formencode.Schema):
 
226
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
227
    role = formencode.All(formencode.validators.OneOf(
 
228
                                ["lecturer", "tutor", "student"]),
 
229
                          RoleEnrolmentValidator(),
 
230
                          formencode.validators.UnicodeString())
 
231
 
 
232
 
 
233
class EnrolmentsView(XHTMLView):
 
234
    """A page which displays all users enrolled in an offering."""
 
235
    template = 'templates/enrolments.html'
 
236
    permission = 'edit'
 
237
 
 
238
    def populate(self, req, ctx):
 
239
        ctx['offering'] = self.context
 
240
 
 
241
class EnrolView(XHTMLView):
 
242
    """A form to enrol a user in an offering."""
 
243
    template = 'templates/enrol.html'
 
244
    tab = 'subjects'
 
245
    permission = 'enrol'
 
246
 
 
247
    def filter(self, stream, ctx):
 
248
        return stream | HTMLFormFiller(data=ctx['data'])
 
249
 
 
250
    def populate(self, req, ctx):
 
251
        if req.method == 'POST':
 
252
            data = dict(req.get_fieldstorage())
 
253
            try:
 
254
                validator = EnrolSchema()
 
255
                req.offering = self.context # XXX: Getting into state.
 
256
                data = validator.to_python(data, state=req)
 
257
                self.context.enrol(data['user'], data['role'])
 
258
                req.store.commit()
 
259
                req.throw_redirect(req.uri)
 
260
            except formencode.Invalid, e:
 
261
                errors = e.unpack_errors()
 
262
        else:
 
263
            data = {}
 
264
            errors = {}
 
265
 
 
266
        ctx['data'] = data or {}
 
267
        ctx['offering'] = self.context
 
268
        ctx['roles_auth'] = self.context.get_permissions(req.user)
 
269
        ctx['errors'] = errors
 
270
 
 
271
class OfferingProjectsView(XHTMLView):
 
272
    """View the projects for an offering."""
 
273
    template = 'templates/offering_projects.html'
 
274
    permission = 'edit'
 
275
    tab = 'subjects'
 
276
 
 
277
    def populate(self, req, ctx):
 
278
        self.plugin_styles[Plugin] = ["project.css"]
 
279
        self.plugin_scripts[Plugin] = ["project.js"]
 
280
        ctx['req'] = req
 
281
        ctx['offering'] = self.context
 
282
        ctx['projectsets'] = []
 
283
        ctx['OfferingRESTView'] = OfferingRESTView
 
284
 
 
285
        #Open the projectset Fragment, and render it for inclusion
 
286
        #into the ProjectSets page
 
287
        #XXX: This could be a lot cleaner
 
288
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
289
 
 
290
        set_fragment = os.path.join(os.path.dirname(__file__),
 
291
                "templates/projectset_fragment.html")
 
292
        project_fragment = os.path.join(os.path.dirname(__file__),
 
293
                "templates/project_fragment.html")
 
294
 
 
295
        for projectset in self.context.project_sets:
 
296
            settmpl = loader.load(set_fragment)
 
297
            setCtx = Context()
 
298
            setCtx['req'] = req
 
299
            setCtx['projectset'] = projectset
 
300
            setCtx['projects'] = []
 
301
            setCtx['GroupsView'] = GroupsView
 
302
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
303
 
 
304
            for project in projectset.projects:
 
305
                projecttmpl = loader.load(project_fragment)
 
306
                projectCtx = Context()
 
307
                projectCtx['req'] = req
 
308
                projectCtx['project'] = project
 
309
 
 
310
                setCtx['projects'].append(
 
311
                        projecttmpl.generate(projectCtx))
 
312
 
 
313
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
314
 
 
315
 
 
316
class ProjectView(XHTMLView):
 
317
    """View the submissions for a ProjectSet"""
 
318
    template = "templates/project.html"
 
319
    permission = "edit"
 
320
    tab = 'subjects'
 
321
 
 
322
    def build_subversion_url(self, svnroot, submission):
 
323
        princ = submission.assessed.principal
 
324
 
 
325
        if isinstance(princ, User):
 
326
            path = 'users/%s' % princ.login
 
327
        else:
 
328
            path = 'groups/%s_%s_%s_%s' % (
 
329
                    princ.project_set.offering.subject.short_name,
 
330
                    princ.project_set.offering.semester.year,
 
331
                    princ.project_set.offering.semester.semester,
 
332
                    princ.name
 
333
                    )
 
334
        return urlparse.urljoin(
 
335
                    svnroot,
 
336
                    os.path.join(path, submission.path[1:] if
 
337
                                       submission.path.startswith(os.sep) else
 
338
                                       submission.path))
 
339
 
 
340
    def populate(self, req, ctx):
 
341
        self.plugin_styles[Plugin] = ["project.css"]
 
342
 
 
343
        ctx['req'] = req
 
344
        ctx['GroupsView'] = GroupsView
 
345
        ctx['EnrolView'] = EnrolView
 
346
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
347
        ctx['build_subversion_url'] = self.build_subversion_url
 
348
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
349
        ctx['project'] = self.context
 
350
        ctx['user'] = req.user
 
351
 
 
352
class Plugin(ViewPlugin, MediaPlugin):
 
353
    forward_routes = (root_to_subject, subject_to_offering,
 
354
                      offering_to_project, offering_to_projectset)
 
355
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
356
 
 
357
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
358
             (Offering, '+index', OfferingView),
 
359
             (Offering, '+edit', OfferingEdit),
 
360
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
361
             (Offering, ('+enrolments', '+new'), EnrolView),
 
362
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
363
             (Project, '+index', ProjectView),
 
364
 
 
365
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
366
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
367
             ]
 
368
 
 
369
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
370
                   Offering: OfferingBreadcrumb,
 
371
                   User: UserBreadcrumb,
 
372
                   Project: ProjectBreadcrumb,
 
373
                   }
 
374
 
 
375
    tabs = [
 
376
        ('subjects', 'Subjects',
 
377
         'View subject content and complete worksheets',
 
378
         'subjects.png', 'subjects', 5)
 
379
    ]
 
380
 
 
381
    media = 'subject-media'