~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
 
 
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
 
29
37
from ivle.webapp.base.xhtml import XHTMLView
30
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
 
from ivle.webapp.errors import NotFound
32
 
from ivle.database import Subject
 
39
from ivle.webapp import ApplicationRoot
 
40
 
 
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
42
                          ProjectSet, Project, ProjectSubmission
33
43
from ivle import util
 
44
import ivle.date
34
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
35
55
 
36
56
class SubjectsView(XHTMLView):
37
57
    '''The view of the list of subjects.'''
38
 
    template = 'subjects.html'
39
 
    appname = 'subjects' # XXX
 
58
    template = 'templates/subjects.html'
 
59
    tab = 'subjects'
40
60
 
41
61
    def authorize(self, req):
42
62
        return req.user is not None
43
63
 
44
64
    def populate(self, req, ctx):
45
 
        enrolled_subjects = req.user.subjects
46
 
        unenrolled_subjects = [subject for subject in
47
 
                               req.store.find(Subject)
48
 
                               if subject not in enrolled_subjects]
49
 
 
50
 
        ctx['enrolled_subjects'] = []
51
 
        ctx['other_subjects'] = []
52
 
 
53
 
        req.content_type = "text/html"
54
 
        req.write_html_head_foot = True
55
 
 
56
 
        for subject in enrolled_subjects:
57
 
            new_subj = {}
58
 
            new_subj['name'] = subject.name
59
 
            new_subj['url'] = subject.url
60
 
            ctx['enrolled_subjects'].append(new_subj)
61
 
 
62
 
        if len(unenrolled_subjects) > 0:
63
 
            for subject in unenrolled_subjects:
64
 
                new_subj = {}
65
 
                new_subj['name'] = subject.name
66
 
                new_subj['url'] = subject.url
67
 
                ctx['other_subjects'].append(new_subj)
68
 
 
 
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
        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
 
187
 
 
188
 
 
189
class UserValidator(formencode.FancyValidator):
 
190
    """A FormEncode validator that turns a username into a user.
 
191
 
 
192
    The state must have a 'store' attribute, which is the Storm store
 
193
    to use."""
 
194
    def _to_python(self, value, state):
 
195
        user = User.get_by_login(state.store, value)
 
196
        if user:
 
197
            return user
 
198
        else:
 
199
            raise formencode.Invalid('User does not exist', value, state)
 
200
 
 
201
 
 
202
class NoEnrolmentValidator(formencode.FancyValidator):
 
203
    """A FormEncode validator that ensures absence of an enrolment.
 
204
 
 
205
    The state must have an 'offering' attribute.
 
206
    """
 
207
    def _to_python(self, value, state):
 
208
        if state.offering.get_enrolment(value):
 
209
            raise formencode.Invalid('User already enrolled', value, state)
 
210
        return value
 
211
 
 
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
 
 
226
class EnrolSchema(formencode.Schema):
 
227
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
 
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
 
241
 
 
242
class EnrolView(XHTMLView):
 
243
    """A form to enrol a user in an offering."""
 
244
    template = 'templates/enrol.html'
 
245
    tab = 'subjects'
 
246
    permission = 'enrol'
 
247
 
 
248
    def filter(self, stream, ctx):
 
249
        return stream | HTMLFormFiller(data=ctx['data'])
 
250
 
 
251
    def populate(self, req, ctx):
 
252
        if req.method == 'POST':
 
253
            data = dict(req.get_fieldstorage())
 
254
            try:
 
255
                validator = EnrolSchema()
 
256
                req.offering = self.context # XXX: Getting into state.
 
257
                data = validator.to_python(data, state=req)
 
258
                self.context.enrol(data['user'], data['role'])
 
259
                req.store.commit()
 
260
                req.throw_redirect(req.uri)
 
261
            except formencode.Invalid, e:
 
262
                errors = e.unpack_errors()
 
263
        else:
 
264
            data = {}
 
265
            errors = {}
 
266
 
 
267
        ctx['data'] = data or {}
 
268
        ctx['offering'] = self.context
 
269
        ctx['roles_auth'] = self.context.get_permissions(req.user)
 
270
        ctx['errors'] = errors
 
271
 
 
272
class OfferingProjectsView(XHTMLView):
 
273
    """View the projects for an offering."""
 
274
    template = 'templates/offering_projects.html'
 
275
    permission = 'edit'
 
276
    tab = 'subjects'
 
277
 
 
278
    def populate(self, req, ctx):
 
279
        self.plugin_styles[Plugin] = ["project.css"]
 
280
        self.plugin_scripts[Plugin] = ["project.js"]
 
281
        ctx['req'] = req
 
282
        ctx['offering'] = self.context
 
283
        ctx['projectsets'] = []
 
284
        ctx['OfferingRESTView'] = OfferingRESTView
 
285
 
 
286
        #Open the projectset Fragment, and render it for inclusion
 
287
        #into the ProjectSets page
 
288
        #XXX: This could be a lot cleaner
 
289
        loader = genshi.template.TemplateLoader(".", auto_reload=True)
 
290
 
 
291
        set_fragment = os.path.join(os.path.dirname(__file__),
 
292
                "templates/projectset_fragment.html")
 
293
        project_fragment = os.path.join(os.path.dirname(__file__),
 
294
                "templates/project_fragment.html")
 
295
 
 
296
        for projectset in self.context.project_sets:
 
297
            settmpl = loader.load(set_fragment)
 
298
            setCtx = Context()
 
299
            setCtx['req'] = req
 
300
            setCtx['projectset'] = projectset
 
301
            setCtx['projects'] = []
 
302
            setCtx['GroupsView'] = GroupsView
 
303
            setCtx['ProjectSetRESTView'] = ProjectSetRESTView
 
304
 
 
305
            for project in projectset.projects:
 
306
                projecttmpl = loader.load(project_fragment)
 
307
                projectCtx = Context()
 
308
                projectCtx['req'] = req
 
309
                projectCtx['project'] = project
 
310
 
 
311
                setCtx['projects'].append(
 
312
                        projecttmpl.generate(projectCtx))
 
313
 
 
314
            ctx['projectsets'].append(settmpl.generate(setCtx))
 
315
 
 
316
 
 
317
class ProjectView(XHTMLView):
 
318
    """View the submissions for a ProjectSet"""
 
319
    template = "templates/project.html"
 
320
    permission = "edit"
 
321
    tab = 'subjects'
 
322
 
 
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))
 
340
 
 
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
 
347
        ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
 
348
        ctx['build_subversion_url'] = self.build_subversion_url
 
349
        ctx['svn_addr'] = req.config['urls']['svn_addr']
 
350
        ctx['project'] = self.context
 
351
        ctx['user'] = req.user
69
352
 
70
353
class Plugin(ViewPlugin, MediaPlugin):
71
 
    urls = [
72
 
        ('subjects/', SubjectsView),
73
 
    ]
 
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
                   }
74
375
 
75
376
    tabs = [
76
 
        ('subjects', 'Subjects', 'Announcements and information about the '
77
 
         'subjects you are enrolled in.', 'subjects.png', 'subjects', 5)
 
377
        ('subjects', 'Subjects',
 
378
         'View subject content and complete worksheets',
 
379
         'subjects.png', 'subjects', 5)
78
380
    ]
79
381
 
80
382
    media = 'subject-media'