~ubuntu-branches/ubuntu/natty/schooltool.gradebook/natty

« back to all changes in this revision

Viewing changes to src/schooltool/gradebook/gradebook.py

  • Committer: Bazaar Package Importer
  • Author(s): Gediminas Paulauskas
  • Date: 2011-02-24 16:53:53 UTC
  • Revision ID: james.westby@ubuntu.com-20110224165353-hi69ckyal3b8dyns
Tags: upstream-0.9.0~a1
ImportĀ upstreamĀ versionĀ 0.9.0~a1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# SchoolTool - common information systems platform for school administration
 
3
# Copyright (c) 2005 Shuttleworth Foundation
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
#
 
19
"""
 
20
Gradebook Implementation
 
21
"""
 
22
__docformat__ = 'reStructuredText'
 
23
 
 
24
from decimal import Decimal
 
25
 
 
26
from persistent.dict import PersistentDict
 
27
from zope.security import proxy
 
28
from zope import annotation
 
29
from zope.keyreference.interfaces import IKeyReference
 
30
from zope.component import adapts, queryMultiAdapter, getMultiAdapter
 
31
from zope.interface import implements
 
32
from zope.location.location import LocationProxy
 
33
from zope.publisher.interfaces import IPublishTraverse
 
34
from zope.security.proxy import removeSecurityProxy
 
35
 
 
36
from schooltool import course, requirement
 
37
from schooltool.app.interfaces import ISchoolToolApplication
 
38
from schooltool.basicperson.interfaces import IBasicPerson
 
39
from schooltool.securitypolicy.crowds import ConfigurableCrowd
 
40
from schooltool.securitypolicy.crowds import AdministrationCrowd
 
41
 
 
42
from schooltool.gradebook import interfaces
 
43
from schooltool.gradebook.activity import getSourceObj
 
44
from schooltool.gradebook.activity import ensureAtLeastOneWorksheet
 
45
from schooltool.requirement.scoresystem import UNSCORED, ScoreValidationError
 
46
from schooltool.requirement.interfaces import IDiscreteValuesScoreSystem
 
47
from schooltool.requirement.interfaces import IRangedValuesScoreSystem
 
48
from schooltool.requirement.scoresystem import RangedValuesScoreSystem
 
49
 
 
50
GRADEBOOK_SORTING_KEY = 'schooltool.gradebook.sorting'
 
51
CURRENT_SECTION_TAUGHT_KEY = 'schooltool.gradebook.currentsectiontaught'
 
52
CURRENT_SECTION_ATTENDED_KEY = 'schooltool.gradebook.currentsectionattended'
 
53
CURRENT_WORKSHEET_KEY = 'schooltool.gradebook.currentworksheet'
 
54
DUE_DATE_FILTER_KEY = 'schooltool.gradebook.duedatefilter'
 
55
COLUMN_PREFERENCES_KEY = 'schooltool.gradebook.columnpreferences'
 
56
 
 
57
 
 
58
def getCurrentSectionTaught(person):
 
59
    person = proxy.removeSecurityProxy(person)
 
60
    ann = annotation.interfaces.IAnnotations(person)
 
61
    if CURRENT_SECTION_TAUGHT_KEY not in ann:
 
62
        ann[CURRENT_SECTION_TAUGHT_KEY] = None
 
63
    else:
 
64
        section = ann[CURRENT_SECTION_TAUGHT_KEY]
 
65
        try:
 
66
            interfaces.IActivities(section)
 
67
        except:
 
68
            ann[CURRENT_SECTION_TAUGHT_KEY] = None
 
69
    return ann[CURRENT_SECTION_TAUGHT_KEY]
 
70
 
 
71
 
 
72
def setCurrentSectionTaught(person, section):
 
73
    person = proxy.removeSecurityProxy(person)
 
74
    ann = annotation.interfaces.IAnnotations(person)
 
75
    ann[CURRENT_SECTION_TAUGHT_KEY] = section
 
76
 
 
77
 
 
78
def getCurrentSectionAttended(person):
 
79
    person = proxy.removeSecurityProxy(person)
 
80
    ann = annotation.interfaces.IAnnotations(person)
 
81
    if CURRENT_SECTION_ATTENDED_KEY not in ann:
 
82
        ann[CURRENT_SECTION_ATTENDED_KEY] = None
 
83
    else:
 
84
        section = ann[CURRENT_SECTION_ATTENDED_KEY]
 
85
        try:
 
86
            interfaces.IActivities(section)
 
87
        except:
 
88
            ann[CURRENT_SECTION_ATTENDED_KEY] = None
 
89
    return ann[CURRENT_SECTION_ATTENDED_KEY]
 
90
 
 
91
 
 
92
def setCurrentSectionAttended(person, section):
 
93
    person = proxy.removeSecurityProxy(person)
 
94
    ann = annotation.interfaces.IAnnotations(person)
 
95
    ann[CURRENT_SECTION_ATTENDED_KEY] = section
 
96
 
 
97
 
 
98
class WorksheetGradebookTraverser(object):
 
99
    '''Traverser that goes from a worksheet to the gradebook'''
 
100
 
 
101
    implements(IPublishTraverse)
 
102
 
 
103
    def __init__(self, context, request):
 
104
        self.context = context
 
105
        self.request = request
 
106
 
 
107
    def publishTraverse(self, request, name):
 
108
        context = proxy.removeSecurityProxy(self.context)
 
109
        try:
 
110
            activity = context[name]
 
111
            return activity
 
112
        except KeyError:
 
113
            if name == 'gradebook':
 
114
                gb = interfaces.IGradebook(context)
 
115
                gb = LocationProxy(gb, self.context, name)
 
116
                gb.__setattr__('__parent__', gb.__parent__)
 
117
                return gb
 
118
            elif name == 'mygrades':
 
119
                gb = interfaces.IMyGrades(context)
 
120
                gb = LocationProxy(gb, self.context, name)
 
121
                gb.__setattr__('__parent__', gb.__parent__)
 
122
                return gb
 
123
            else:
 
124
                return queryMultiAdapter((self.context, request), name=name)
 
125
 
 
126
 
 
127
class StudentGradebookTraverser(object):
 
128
    '''Traverser that goes from a section's gradebook to a student
 
129
    gradebook using the student's username as the path in the url.'''
 
130
 
 
131
    implements(IPublishTraverse)
 
132
 
 
133
    def __init__(self, context, request):
 
134
        self.context = context
 
135
        self.request = request
 
136
 
 
137
    def publishTraverse(self, request, name):
 
138
        app = ISchoolToolApplication(None)
 
139
        context = removeSecurityProxy(self.context)
 
140
 
 
141
        try:
 
142
            student = app['persons'][name]
 
143
        except KeyError:
 
144
            return queryMultiAdapter((self.context, request), name=name)
 
145
 
 
146
        try:
 
147
            gb = getMultiAdapter((student, context), interfaces.IStudentGradebook)
 
148
        except ValueError:
 
149
            return queryMultiAdapter((self.context, request), name=name)
 
150
 
 
151
        # location looks like http://host/path/to/gradebook/studentsUsername
 
152
        gb = LocationProxy(gb, self.context, name)
 
153
        return gb
 
154
 
 
155
 
 
156
class GradebookBase(object):
 
157
    def __init__(self, context):
 
158
        self.context = context
 
159
        # To make URL creation happy
 
160
        self.__parent__ = context
 
161
        self.section = self.context.__parent__.__parent__
 
162
        # Establish worksheets and all activities
 
163
        activities = interfaces.IActivities(self.section)
 
164
        ensureAtLeastOneWorksheet(activities)
 
165
        self.worksheets = list(activities.values())
 
166
        self.activities = []
 
167
        for activity in context.values():
 
168
            self.activities.append(activity)
 
169
        self.students = list(self.section.members)
 
170
 
 
171
    def _checkStudent(self, student):
 
172
        if student not in self.students:
 
173
            raise ValueError(
 
174
                'Student %r is not in this section.' %student.username)
 
175
        # Remove security proxy, so that the object can be referenced and
 
176
        # adapters are not proxied. Note that the gradebook itself has
 
177
        # sufficient tight security.
 
178
        return proxy.removeSecurityProxy(student)
 
179
 
 
180
    def _checkActivity(self, activity):
 
181
        # Remove security proxy, so that the object can be referenced and
 
182
        # adapters are not proxied. Note that the gradebook itself has
 
183
        # sufficient tight security.
 
184
        if activity in self.activities:
 
185
            return proxy.removeSecurityProxy(activity)
 
186
        raise ValueError(
 
187
            '%r is not part of this section.' %activity.title)
 
188
 
 
189
    def hasEvaluation(self, student, activity):
 
190
        """See interfaces.IGradebook"""
 
191
        student = self._checkStudent(student)
 
192
        activity = self._checkActivity(activity)
 
193
        if activity in requirement.interfaces.IEvaluations(student):
 
194
            return True
 
195
        return False
 
196
 
 
197
    def getEvaluation(self, student, activity):
 
198
        """See interfaces.IGradebook"""
 
199
        student = self._checkStudent(student)
 
200
        activity = self._checkActivity(activity)
 
201
        evaluations = requirement.interfaces.IEvaluations(student)
 
202
        ev, value, ss = None, None, None
 
203
        if interfaces.ILinkedColumnActivity.providedBy(activity):
 
204
            sourceObj = getSourceObj(activity.source)
 
205
            if interfaces.IActivity.providedBy(sourceObj):
 
206
                ev = evaluations.get(sourceObj, None)
 
207
                ss = sourceObj.scoresystem
 
208
            elif interfaces.IWorksheet.providedBy(sourceObj):
 
209
                gb = interfaces.IGradebook(sourceObj)
 
210
                if student in gb.students:
 
211
                    total, value = gb.getWorksheetTotalAverage(sourceObj, student)
 
212
                    ss = RangedValuesScoreSystem()
 
213
        else:
 
214
            ev = evaluations.get(activity, None)
 
215
            ss = activity.scoresystem
 
216
        if ev is not None and ev.value is not UNSCORED:
 
217
            value = ev.value
 
218
        return value, ss
 
219
 
 
220
    def evaluate(self, student, activity, score, evaluator=None):
 
221
        """See interfaces.IGradebook"""
 
222
        student = self._checkStudent(student)
 
223
        activity = self._checkActivity(activity)
 
224
        evaluation = requirement.evaluation.Evaluation(
 
225
            activity, activity.scoresystem, score, evaluator)
 
226
        evaluations = requirement.interfaces.IEvaluations(student)
 
227
        evaluations.addEvaluation(evaluation)
 
228
 
 
229
    def removeEvaluation(self, student, activity):
 
230
        """See interfaces.IGradebook"""
 
231
        student = self._checkStudent(student)
 
232
        activity = self._checkActivity(activity)
 
233
        evaluations = requirement.interfaces.IEvaluations(student)
 
234
        del evaluations[activity]
 
235
 
 
236
    def getWorksheetActivities(self, worksheet):
 
237
        if worksheet:
 
238
            return list(worksheet.values())
 
239
        else:
 
240
            return []
 
241
 
 
242
    def getWorksheetTotalAverage(self, worksheet, student):
 
243
        if worksheet is None:
 
244
            return 0, UNSCORED
 
245
        weights = worksheet.getCategoryWeights()
 
246
 
 
247
        # weight by categories
 
248
        if weights:
 
249
            adjusted_weights = {}
 
250
            for activity in self.getWorksheetActivities(worksheet):
 
251
                value, ss = self.getEvaluation(student, activity)
 
252
                category = activity.category
 
253
                if value is not None and value is not UNSCORED:
 
254
                    if category in weights:
 
255
                        adjusted_weights[category] = weights[category]
 
256
            total_percentage = 0
 
257
            for key in adjusted_weights:
 
258
                total_percentage += adjusted_weights[key]
 
259
            for key in adjusted_weights:
 
260
                adjusted_weights[key] /= total_percentage
 
261
 
 
262
            totals = {}
 
263
            average_totals = {}
 
264
            average_counts = {}
 
265
            for activity in self.getWorksheetActivities(worksheet):
 
266
                value, ss = self.getEvaluation(student, activity)
 
267
                if value is not None and value is not UNSCORED:
 
268
                    if IDiscreteValuesScoreSystem.providedBy(ss):
 
269
                        minimum = ss.scores[-1][2]
 
270
                        maximum = ss.scores[0][2]
 
271
                        value = ss.getNumericalValue(value)
 
272
                    elif IRangedValuesScoreSystem.providedBy(ss):
 
273
                        minimum = ss.min
 
274
                        maximum = ss.max
 
275
                    else:
 
276
                        continue
 
277
                    totals.setdefault(activity.category, Decimal(0))
 
278
                    totals[activity.category] += value - minimum
 
279
                    average_totals.setdefault(activity.category, Decimal(0))
 
280
                    average_totals[activity.category] += (value - minimum)
 
281
                    average_counts.setdefault(activity.category, Decimal(0))
 
282
                    average_counts[activity.category] += (maximum - minimum)
 
283
            average = Decimal(0)
 
284
            for category, value in average_totals.items():
 
285
                if category in weights:
 
286
                    average += ((value / average_counts[category]) *
 
287
                        adjusted_weights[category])
 
288
            if not len(average_counts):
 
289
                return 0, UNSCORED
 
290
            else:
 
291
                return sum(totals.values()), average * 100
 
292
 
 
293
        # when not weighting categories, the default is to weight the
 
294
        # evaluations by activities.
 
295
        else:
 
296
            total = 0
 
297
            count = 0
 
298
            for activity in self.getWorksheetActivities(worksheet):
 
299
                value, ss = self.getEvaluation(student, activity)
 
300
                if value is not None and value is not UNSCORED:
 
301
                    if IDiscreteValuesScoreSystem.providedBy(ss):
 
302
                        minimum = ss.scores[-1][2]
 
303
                        maximum = ss.scores[0][2]
 
304
                        value = ss.getNumericalValue(value)
 
305
                    elif IRangedValuesScoreSystem.providedBy(ss):
 
306
                        minimum = ss.min
 
307
                        maximum = ss.max
 
308
                    else:
 
309
                        continue
 
310
                    total += value - minimum
 
311
                    count += maximum - minimum
 
312
            if count:
 
313
                return total, Decimal(100 * total) / Decimal(count)
 
314
            else:
 
315
                return 0, UNSCORED
 
316
 
 
317
    def getCurrentWorksheet(self, person):
 
318
        person = proxy.removeSecurityProxy(person)
 
319
        ann = annotation.interfaces.IAnnotations(person)
 
320
        if CURRENT_WORKSHEET_KEY not in ann:
 
321
            ann[CURRENT_WORKSHEET_KEY] = PersistentDict()
 
322
        if self.worksheets:
 
323
            default = self.worksheets[0]
 
324
        else:
 
325
            default = None
 
326
        section_id = hash(IKeyReference(self.section))
 
327
        worksheet = ann[CURRENT_WORKSHEET_KEY].get(section_id, default)
 
328
        if worksheet is not None and worksheet.hidden:
 
329
            return default
 
330
        return worksheet
 
331
 
 
332
    def setCurrentWorksheet(self, person, worksheet):
 
333
        person = proxy.removeSecurityProxy(person)
 
334
        worksheet = proxy.removeSecurityProxy(worksheet)
 
335
        ann = annotation.interfaces.IAnnotations(person)
 
336
        if CURRENT_WORKSHEET_KEY not in ann:
 
337
            ann[CURRENT_WORKSHEET_KEY] = PersistentDict()
 
338
        section_id = hash(IKeyReference(self.section))
 
339
        ann[CURRENT_WORKSHEET_KEY][section_id] = worksheet
 
340
 
 
341
    def getDueDateFilter(self, person):
 
342
        person = proxy.removeSecurityProxy(person)
 
343
        ann = annotation.interfaces.IAnnotations(person)
 
344
        if DUE_DATE_FILTER_KEY not in ann:
 
345
            return (False, '9')
 
346
        return ann[DUE_DATE_FILTER_KEY]
 
347
 
 
348
    def setDueDateFilter(self, person, flag, weeks):
 
349
        person = proxy.removeSecurityProxy(person)
 
350
        ann = annotation.interfaces.IAnnotations(person)
 
351
        ann[DUE_DATE_FILTER_KEY] = (flag, weeks)
 
352
 
 
353
    def getColumnPreferences(self, person):
 
354
        person = proxy.removeSecurityProxy(person)
 
355
        ann = annotation.interfaces.IAnnotations(person)
 
356
        if COLUMN_PREFERENCES_KEY not in ann:
 
357
            return PersistentDict()
 
358
        return ann[COLUMN_PREFERENCES_KEY]
 
359
 
 
360
    def setColumnPreferences(self, person, columnPreferences):
 
361
        person = proxy.removeSecurityProxy(person)
 
362
        ann = annotation.interfaces.IAnnotations(person)
 
363
        ann[COLUMN_PREFERENCES_KEY] = PersistentDict(columnPreferences)
 
364
 
 
365
    def getCurrentActivities(self, person):
 
366
        worksheet = self.getCurrentWorksheet(person)
 
367
        return self.getWorksheetActivities(worksheet)
 
368
 
 
369
    def getCurrentEvaluationsForStudent(self, person, student):
 
370
        """See interfaces.IGradebook"""
 
371
        self._checkStudent(student)
 
372
        evaluations = requirement.interfaces.IEvaluations(student)
 
373
        activities = self.getCurrentActivities(person)
 
374
        for activity, evaluation in evaluations.items():
 
375
            if activity in activities:
 
376
                yield activity, evaluation
 
377
 
 
378
    def getEvaluationsForStudent(self, student):
 
379
        """See interfaces.IGradebook"""
 
380
        self._checkStudent(student)
 
381
        evaluations = requirement.interfaces.IEvaluations(student)
 
382
        for activity, evaluation in evaluations.items():
 
383
            if activity in self.activities:
 
384
                yield activity, evaluation
 
385
 
 
386
    def getEvaluationsForActivity(self, activity):
 
387
        """See interfaces.IGradebook"""
 
388
        self._checkActivity(activity)
 
389
        for student in self.section.members:
 
390
            evaluations = requirement.interfaces.IEvaluations(student)
 
391
            if activity in evaluations:
 
392
                yield student, evaluations[activity]
 
393
 
 
394
    def getSortKey(self, person):
 
395
        person = proxy.removeSecurityProxy(person)
 
396
        ann = annotation.interfaces.IAnnotations(person)
 
397
        if GRADEBOOK_SORTING_KEY not in ann:
 
398
            ann[GRADEBOOK_SORTING_KEY] = PersistentDict()
 
399
        section_id = hash(IKeyReference(self.section))
 
400
        return ann[GRADEBOOK_SORTING_KEY].get(section_id, ('student', False))
 
401
 
 
402
    def setSortKey(self, person, value):
 
403
        person = proxy.removeSecurityProxy(person)
 
404
        ann = annotation.interfaces.IAnnotations(person)
 
405
        if GRADEBOOK_SORTING_KEY not in ann:
 
406
            ann[GRADEBOOK_SORTING_KEY] = PersistentDict()
 
407
        section_id = hash(IKeyReference(self.section))
 
408
        ann[GRADEBOOK_SORTING_KEY][section_id] = value
 
409
 
 
410
 
 
411
class Gradebook(GradebookBase):
 
412
    implements(interfaces.IGradebook)
 
413
    adapts(interfaces.IWorksheet)
 
414
 
 
415
    def __init__(self, context):
 
416
        super(Gradebook, self).__init__(context)
 
417
        # To make URL creation happy
 
418
        self.__name__ = 'gradebook'
 
419
 
 
420
 
 
421
class MyGrades(GradebookBase):
 
422
    implements(interfaces.IMyGrades)
 
423
    adapts(interfaces.IWorksheet)
 
424
 
 
425
    def __init__(self, context):
 
426
        super(MyGrades, self).__init__(context)
 
427
        # To make URL creation happy
 
428
        self.__name__ = 'mygrades'
 
429
 
 
430
 
 
431
class StudentGradebook(object):
 
432
    """Adapter of student and gradebook used for grading one student at a
 
433
       time"""
 
434
    implements(interfaces.IStudentGradebook)
 
435
    adapts(IBasicPerson, interfaces.IGradebook)
 
436
 
 
437
    def __init__(self, student, gradebook):
 
438
        self.student = student
 
439
        self.gradebook = gradebook
 
440
        activities = [(str(activity.__name__), activity)
 
441
            for activity in gradebook.activities]
 
442
        self.activities = dict(activities)
 
443
 
 
444
 
 
445
class StudentGradebookFormAdapter(object):
 
446
    """Adapter used by grade student view to interact with student
 
447
       gradebook"""
 
448
    implements(interfaces.IStudentGradebookForm)
 
449
    adapts(interfaces.IStudentGradebook)
 
450
 
 
451
    def __init__(self, context):
 
452
        self.__dict__['context'] = context
 
453
 
 
454
    def __setattr__(self, name, value):
 
455
        gradebook = self.context.gradebook
 
456
        student = self.context.student
 
457
        activity = self.context.activities[name]
 
458
        evaluator = None
 
459
        try:
 
460
            if value is None or value == '':
 
461
                score, ss = gradebook.getEvaluation(student, activity)
 
462
                if score is not None:
 
463
                    gradebook.removeEvaluation(student, activity)
 
464
            else:
 
465
                score = activity.scoresystem.fromUnicode(value)
 
466
                gradebook.evaluate(student, activity, score, evaluator)
 
467
        except ScoreValidationError:
 
468
            pass
 
469
 
 
470
    def __getattr__(self, name):
 
471
        activity = self.context.activities[name]
 
472
        value, ss = self.context.gradebook.getEvaluation(self.context.student,
 
473
            activity)
 
474
        if value is None or value is UNSCORED:
 
475
            value = ''
 
476
        return value
 
477
 
 
478
 
 
479
def getWorksheetSection(worksheet):
 
480
    """Adapt IWorksheet to ISection."""
 
481
    return worksheet.__parent__.__parent__
 
482
 
 
483
 
 
484
def getGradebookSection(gradebook):
 
485
    """Adapt IGradebook to ISection."""
 
486
    return course.interfaces.ISection(gradebook.context)
 
487
 
 
488
 
 
489
def getMyGradesSection(gradebook):
 
490
    """Adapt IMyGrades to ISection."""
 
491
    return course.interfaces.ISection(gradebook.context)
 
492
 
 
493
 
 
494
class GradebookEditorsCrowd(ConfigurableCrowd):
 
495
    setting_key = 'administration_can_grade_students'
 
496
 
 
497
    def contains(self, principal):
 
498
        """Return the value of the related setting (True or False)."""
 
499
        return (AdministrationCrowd(self.context).contains(principal) and
 
500
                super(GradebookEditorsCrowd, self).contains(principal))
 
501