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

« back to all changes in this revision

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

  • Committer: Matt Giuca
  • Date: 2010-02-04 04:43:37 UTC
  • Revision ID: matt.giuca@gmail.com-20100204044337-6114sl82txql6g85
doc/dev/sample.rst: Refer to subjects by short name, not subj code.

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
 
32
import genshi
30
33
from genshi.filters import HTMLFormFiller
 
34
from genshi.template import Context, TemplateLoader
31
35
import formencode
32
36
 
33
37
from ivle.webapp.base.xhtml import XHTMLView
34
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
35
 
from ivle.webapp.errors import NotFound
36
 
from ivle.database import Subject, Semester, Offering, Enrolment, User
 
39
from ivle.webapp import ApplicationRoot
 
40
 
 
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
 
42
                          ProjectSet, Project, ProjectSubmission
37
43
from ivle import util
 
44
import ivle.date
38
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
39
55
 
40
56
class SubjectsView(XHTMLView):
41
57
    '''The view of the list of subjects.'''
42
 
    template = 'subjects.html'
 
58
    template = 'templates/subjects.html'
43
59
    tab = 'subjects'
44
60
 
45
61
    def authorize(self, req):
50
66
        ctx['semesters'] = []
51
67
        for semester in req.store.find(Semester).order_by(Desc(Semester.year),
52
68
                                                     Desc(Semester.semester)):
53
 
            enrolments = semester.enrolments.find(user=req.user)
54
 
            if enrolments.count():
55
 
                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
56
187
 
57
188
 
58
189
class UserValidator(formencode.FancyValidator):
79
210
        return value
80
211
 
81
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
 
82
226
class EnrolSchema(formencode.Schema):
83
227
    user = formencode.All(NoEnrolmentValidator(), UserValidator())
84
 
 
 
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
85
241
 
86
242
class EnrolView(XHTMLView):
87
243
    """A form to enrol a user in an offering."""
88
 
    template = 'enrol.html'
 
244
    template = 'templates/enrol.html'
89
245
    tab = 'subjects'
90
 
    permission = 'edit'
91
 
 
92
 
    def __init__(self, req, subject, year, semester):
93
 
        """Find the given offering by subject, year and semester."""
94
 
        self.context = req.store.find(Offering,
95
 
            Offering.subject_id == Subject.id,
96
 
            Subject.short_name == subject,
97
 
            Offering.semester_id == Semester.id,
98
 
            Semester.year == year,
99
 
            Semester.semester == semester).one()
100
 
 
101
 
        if not self.context:
102
 
            raise NotFound()
 
246
    permission = 'enrol'
103
247
 
104
248
    def filter(self, stream, ctx):
105
249
        return stream | HTMLFormFiller(data=ctx['data'])
111
255
                validator = EnrolSchema()
112
256
                req.offering = self.context # XXX: Getting into state.
113
257
                data = validator.to_python(data, state=req)
114
 
                self.context.enrol(data['user'])
 
258
                self.context.enrol(data['user'], data['role'])
115
259
                req.store.commit()
116
260
                req.throw_redirect(req.uri)
117
261
            except formencode.Invalid, e:
122
266
 
123
267
        ctx['data'] = data or {}
124
268
        ctx['offering'] = self.context
 
269
        ctx['roles_auth'] = self.context.get_permissions(req.user)
125
270
        ctx['errors'] = errors
126
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
127
352
 
128
353
class Plugin(ViewPlugin, MediaPlugin):
129
 
    urls = [
130
 
        ('subjects/', SubjectsView),
131
 
        ('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
132
 
    ]
 
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
                   }
133
375
 
134
376
    tabs = [
135
377
        ('subjects', 'Subjects',