~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 05:09:56 UTC
  • Revision ID: grantw@unimelb.edu.au-20100211050956-t5i2z6b8iulxteza
Unbreak existing tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
# A sample / testing application for IVLE.
24
24
 
25
25
import os
 
26
import os.path
26
27
import urllib
 
28
import urlparse
27
29
import cgi
28
30
 
29
 
from storm.locals import Desc
 
31
from storm.locals import Desc, Store
30
32
import genshi
31
33
from genshi.filters import HTMLFormFiller
32
34
from genshi.template import Context, TemplateLoader
34
36
 
35
37
from ivle.webapp.base.xhtml import XHTMLView
36
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
37
 
from ivle.webapp.errors import NotFound
 
39
from ivle.webapp import ApplicationRoot
38
40
 
39
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
40
42
                          ProjectSet, Project, ProjectSubmission
41
43
from ivle import util
42
44
import ivle.date
43
45
 
44
 
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
45
 
                                             ProjectRESTView
 
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView
46
47
from ivle.webapp.admin.offeringservice import OfferingRESTView
47
 
 
 
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
48
55
 
49
56
class SubjectsView(XHTMLView):
50
57
    '''The view of the list of subjects.'''
59
66
        ctx['semesters'] = []
60
67
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
61
68
                                                     Desc(Semester.semester)):
62
 
            enrolments = semester.enrolments.find(user=req.user)
63
 
            if enrolments.count():
64
 
                ctx['semesters'].append((semester, enrolments))
 
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
        if_missing=None, not_empty=False)
 
153
    url = formencode.validators.URL(if_missing=None, not_empty=False)
 
154
 
 
155
 
 
156
class OfferingEdit(XHTMLView):
 
157
    """A form to edit an offering's details."""
 
158
    template = 'templates/offering-edit.html'
 
159
    permission = 'edit'
 
160
 
 
161
    def filter(self, stream, ctx):
 
162
        return stream | HTMLFormFiller(data=ctx['data'])
 
163
 
 
164
    def populate(self, req, ctx):
 
165
        if req.method == 'POST':
 
166
            data = dict(req.get_fieldstorage())
 
167
            try:
 
168
                validator = OfferingSchema()
 
169
                data = validator.to_python(data, state=req)
 
170
 
 
171
                self.context.url = unicode(data['url']) if data['url'] else None
 
172
                self.context.description = data['description']
 
173
                req.store.commit()
 
174
                req.throw_redirect(req.publisher.generate(self.context))
 
175
            except formencode.Invalid, e:
 
176
                errors = e.unpack_errors()
 
177
        else:
 
178
            data = {
 
179
                'url': self.context.url,
 
180
                'description': self.context.description,
 
181
            }
 
182
            errors = {}
 
183
 
 
184
        ctx['data'] = data or {}
 
185
        ctx['context'] = self.context
 
186
        ctx['errors'] = errors
65
187
 
66
188
 
67
189
class UserValidator(formencode.FancyValidator):
88
210
        return value
89
211
 
90
212
 
 
213
class RoleEnrolmentValidator(formencode.FancyValidator):
 
214
    """A FormEncode validator that checks permission to enrol users with a
 
215
    particular role.
 
216
 
 
217
    The state must have an 'offering' attribute.
 
218
    """
 
219
    def _to_python(self, value, state):
 
220
        if ("enrol_" + value) not in state.offering.get_permissions(state.user):
 
221
            raise formencode.Invalid('Not allowed to assign users that role',
 
222
                                     value, state)
 
223
        return value
 
224
 
 
225
 
91
226
class EnrolSchema(formencode.Schema):
92
227
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
93
 
 
 
228
    role = formencode.All(formencode.validators.OneOf(
 
229
                                ["lecturer", "tutor", "student"]),
 
230
                          RoleEnrolmentValidator(),
 
231
                          formencode.validators.UnicodeString())
 
232
 
 
233
 
 
234
class EnrolmentsView(XHTMLView):
 
235
    """A page which displays all users enrolled in an offering."""
 
236
    template = 'templates/enrolments.html'
 
237
    permission = 'edit'
 
238
 
 
239
    def populate(self, req, ctx):
 
240
        ctx['offering'] = self.context
94
241
 
95
242
class EnrolView(XHTMLView):
96
243
    """A form to enrol a user in an offering."""
97
244
    template = 'templates/enrol.html'
98
245
    tab = 'subjects'
99
 
    permission = 'edit'
100
 
 
101
 
    def __init__(self, req, subject, year, semester):
102
 
        """Find the given offering by subject, year and semester."""
103
 
        self.context = req.store.find(Offering,
104
 
            Offering.subject_id == Subject.id,
105
 
            Subject.short_name == subject,
106
 
            Offering.semester_id == Semester.id,
107
 
            Semester.year == year,
108
 
            Semester.semester == semester).one()
109
 
 
110
 
        if not self.context:
111
 
            raise NotFound()
 
246
    permission = 'enrol'
112
247
 
113
248
    def filter(self, stream, ctx):
114
249
        return stream | HTMLFormFiller(data=ctx['data'])
120
255
                validator = EnrolSchema()
121
256
                req.offering = self.context # XXX: Getting into state.
122
257
                data = validator.to_python(data, state=req)
123
 
                self.context.enrol(data['user'])
 
258
                self.context.enrol(data['user'], data['role'])
124
259
                req.store.commit()
125
260
                req.throw_redirect(req.uri)
126
261
            except formencode.Invalid, e:
131
266
 
132
267
        ctx['data'] = data or {}
133
268
        ctx['offering'] = self.context
 
269
        ctx['roles_auth'] = self.context.get_permissions(req.user)
134
270
        ctx['errors'] = errors
135
271
 
136
272
class OfferingProjectsView(XHTMLView):
138
274
    template = 'templates/offering_projects.html'
139
275
    permission = 'edit'
140
276
    tab = 'subjects'
141
 
    
142
 
    def __init__(self, req, subject, year, semester):
143
 
        self.context = req.store.find(Offering,
144
 
            Offering.subject_id == Subject.id,
145
 
            Subject.short_name == subject,
146
 
            Offering.semester_id == Semester.id,
147
 
            Semester.year == year,
148
 
            Semester.semester == semester).one()
149
 
 
150
 
        if not self.context:
151
 
            raise NotFound()
152
 
 
153
 
    def project_url(self, projectset, project):
154
 
        return "/subjects/%s/%s/%s/+projects/%s" % (
155
 
                    self.context.subject.short_name,
156
 
                    self.context.semester.year,
157
 
                    self.context.semester.semester,
158
 
                    project.short_name
159
 
                    )
160
 
 
161
 
    def new_project_url(self, projectset):
162
 
        return "/api/subjects/" + self.context.subject.short_name + "/" +\
163
 
                self.context.semester.year + "/" + \
164
 
                self.context.semester.semester + "/+projectsets/" +\
165
 
                str(projectset.id) + "/+projects/+new"
166
 
    
 
277
 
167
278
    def populate(self, req, ctx):
168
279
        self.plugin_styles[Plugin] = ["project.css"]
169
280
        self.plugin_scripts[Plugin] = ["project.js"]
 
281
        ctx['req'] = req
170
282
        ctx['offering'] = self.context
171
 
        ctx['subject'] = self.context.subject.short_name
172
 
        ctx['year'] = self.context.semester.year
173
 
        ctx['semester'] = self.context.semester.semester
174
 
 
175
283
        ctx['projectsets'] = []
 
284
        ctx['OfferingRESTView'] = OfferingRESTView
176
285
 
177
286
        #Open the projectset Fragment, and render it for inclusion
178
287
        #into the ProjectSets page
187
296
        for projectset in self.context.project_sets:
188
297
            settmpl = loader.load(set_fragment)
189
298
            setCtx = Context()
190
 
            setCtx['group_size'] = projectset.max_students_per_group
191
 
            setCtx['projectset_id'] = projectset.id
192
 
            setCtx['new_project_url'] = self.new_project_url(projectset)
 
299
            setCtx['req'] = req
 
300
            setCtx['projectset'] = projectset
193
301
            setCtx['projects'] = []
 
302
            setCtx['GroupsView'] = GroupsView
 
303
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
194
304
 
195
305
            for project in projectset.projects:
196
306
                projecttmpl = loader.load(project_fragment)
197
307
                projectCtx = Context()
 
308
                projectCtx['req'] = req
198
309
                projectCtx['project'] = project
199
 
                projectCtx['project_url'] = self.project_url(projectset, project)
200
310
 
201
311
                setCtx['projects'].append(
202
312
                        projecttmpl.generate(projectCtx))
210
320
    permission = "edit"
211
321
    tab = 'subjects'
212
322
 
213
 
    def __init__(self, req, subject, year, semester, project):
214
 
        self.context = req.store.find(Project,
215
 
                Project.short_name == project,
216
 
                Project.project_set_id == ProjectSet.id,
217
 
                ProjectSet.offering_id == Offering.id,
218
 
                Offering.semester_id == Semester.id,
219
 
                Semester.year == year,
220
 
                Semester.semester == semester,
221
 
                Offering.subject_id == Subject.id,
222
 
                Subject.short_name == subject).one()
223
 
        if self.context is None:
224
 
            raise NotFound()
 
323
    def build_subversion_url(self, svnroot, submission):
 
324
        princ = submission.assessed.principal
 
325
 
 
326
        if isinstance(princ, User):
 
327
            path = 'users/%s' % princ.login
 
328
        else:
 
329
            path = 'groups/%s_%s_%s_%s' % (
 
330
                    princ.project_set.offering.subject.short_name,
 
331
                    princ.project_set.offering.semester.year,
 
332
                    princ.project_set.offering.semester.semester,
 
333
                    princ.name
 
334
                    )
 
335
        return urlparse.urljoin(
 
336
                    svnroot,
 
337
                    os.path.join(path, submission.path[1:] if
 
338
                                       submission.path.startswith(os.sep) else
 
339
                                       submission.path))
225
340
 
226
341
    def populate(self, req, ctx):
 
342
        self.plugin_styles[Plugin] = ["project.css"]
 
343
 
 
344
        ctx['req'] = req
 
345
        ctx['GroupsView'] = GroupsView
 
346
        ctx['EnrolView'] = EnrolView
227
347
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
228
 
 
 
348
        ctx['build_subversion_url'] = self.build_subversion_url
 
349
        ctx['svn_addr'] = req.config['urls']['svn_addr']
229
350
        ctx['project'] = self.context
230
 
        ctx['assesseds'] = self.context.assesseds
231
 
 
232
 
        ctx['submissions'] = []
233
 
        for assessed in self.context.assesseds:
234
 
            if assessed.submissions.count() > 0:
235
 
                ctx['submissions'].append(
236
 
                        assessed.submissions.order_by(ProjectSubmission.date_submitted)[-1])
237
 
 
 
351
        ctx['user'] = req.user
238
352
 
239
353
class Plugin(ViewPlugin, MediaPlugin):
240
 
    urls = [
241
 
        ('subjects/', SubjectsView),
242
 
        ('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
243
 
        ('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
244
 
        ('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
245
 
        #API Views
246
 
        ('api/subjects/:subject/:year/:semester/+projectsets/+new',
247
 
            OfferingRESTView),
248
 
        ('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
249
 
            ProjectSetRESTView),
250
 
        ('api/subjects/:subject/:year/:semester/+projects/:project', 
251
 
            ProjectRESTView),
252
 
 
253
 
    ]
 
354
    forward_routes = (root_to_subject, subject_to_offering,
 
355
                      offering_to_project, offering_to_projectset)
 
356
    reverse_routes = (subject_url, offering_url, projectset_url, project_url)
 
357
 
 
358
    views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
 
359
             (Offering, '+index', OfferingView),
 
360
             (Offering, '+edit', OfferingEdit),
 
361
             (Offering, ('+enrolments', '+index'), EnrolmentsView),
 
362
             (Offering, ('+enrolments', '+new'), EnrolView),
 
363
             (Offering, ('+projects', '+index'), OfferingProjectsView),
 
364
             (Project, '+index', ProjectView),
 
365
 
 
366
             (Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
 
367
             (ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
 
368
             ]
 
369
 
 
370
    breadcrumbs = {Subject: SubjectBreadcrumb,
 
371
                   Offering: OfferingBreadcrumb,
 
372
                   User: UserBreadcrumb,
 
373
                   Project: ProjectBreadcrumb,
 
374
                   }
254
375
 
255
376
    tabs = [
256
377
        ('subjects', 'Subjects',