2
# SchoolTool - common information systems platform for school administration
3
# Copyright (c) 2005 Shuttleworth Foundation
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.
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.
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
20
Gradebook Implementation
22
__docformat__ = 'reStructuredText'
24
from decimal import Decimal
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
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
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
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'
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
64
section = ann[CURRENT_SECTION_TAUGHT_KEY]
66
interfaces.IActivities(section)
68
ann[CURRENT_SECTION_TAUGHT_KEY] = None
69
return ann[CURRENT_SECTION_TAUGHT_KEY]
72
def setCurrentSectionTaught(person, section):
73
person = proxy.removeSecurityProxy(person)
74
ann = annotation.interfaces.IAnnotations(person)
75
ann[CURRENT_SECTION_TAUGHT_KEY] = section
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
84
section = ann[CURRENT_SECTION_ATTENDED_KEY]
86
interfaces.IActivities(section)
88
ann[CURRENT_SECTION_ATTENDED_KEY] = None
89
return ann[CURRENT_SECTION_ATTENDED_KEY]
92
def setCurrentSectionAttended(person, section):
93
person = proxy.removeSecurityProxy(person)
94
ann = annotation.interfaces.IAnnotations(person)
95
ann[CURRENT_SECTION_ATTENDED_KEY] = section
98
class WorksheetGradebookTraverser(object):
99
'''Traverser that goes from a worksheet to the gradebook'''
101
implements(IPublishTraverse)
103
def __init__(self, context, request):
104
self.context = context
105
self.request = request
107
def publishTraverse(self, request, name):
108
context = proxy.removeSecurityProxy(self.context)
110
activity = context[name]
113
if name == 'gradebook':
114
gb = interfaces.IGradebook(context)
115
gb = LocationProxy(gb, self.context, name)
116
gb.__setattr__('__parent__', gb.__parent__)
118
elif name == 'mygrades':
119
gb = interfaces.IMyGrades(context)
120
gb = LocationProxy(gb, self.context, name)
121
gb.__setattr__('__parent__', gb.__parent__)
124
return queryMultiAdapter((self.context, request), name=name)
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.'''
131
implements(IPublishTraverse)
133
def __init__(self, context, request):
134
self.context = context
135
self.request = request
137
def publishTraverse(self, request, name):
138
app = ISchoolToolApplication(None)
139
context = removeSecurityProxy(self.context)
142
student = app['persons'][name]
144
return queryMultiAdapter((self.context, request), name=name)
147
gb = getMultiAdapter((student, context), interfaces.IStudentGradebook)
149
return queryMultiAdapter((self.context, request), name=name)
151
# location looks like http://host/path/to/gradebook/studentsUsername
152
gb = LocationProxy(gb, self.context, name)
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())
167
for activity in context.values():
168
self.activities.append(activity)
169
self.students = list(self.section.members)
171
def _checkStudent(self, student):
172
if student not in self.students:
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)
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)
187
'%r is not part of this section.' %activity.title)
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):
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()
214
ev = evaluations.get(activity, None)
215
ss = activity.scoresystem
216
if ev is not None and ev.value is not UNSCORED:
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)
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]
236
def getWorksheetActivities(self, worksheet):
238
return list(worksheet.values())
242
def getWorksheetTotalAverage(self, worksheet, student):
243
if worksheet is None:
245
weights = worksheet.getCategoryWeights()
247
# weight by categories
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]
257
for key in adjusted_weights:
258
total_percentage += adjusted_weights[key]
259
for key in adjusted_weights:
260
adjusted_weights[key] /= total_percentage
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):
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)
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):
291
return sum(totals.values()), average * 100
293
# when not weighting categories, the default is to weight the
294
# evaluations by activities.
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):
310
total += value - minimum
311
count += maximum - minimum
313
return total, Decimal(100 * total) / Decimal(count)
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()
323
default = self.worksheets[0]
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:
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
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:
346
return ann[DUE_DATE_FILTER_KEY]
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)
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]
360
def setColumnPreferences(self, person, columnPreferences):
361
person = proxy.removeSecurityProxy(person)
362
ann = annotation.interfaces.IAnnotations(person)
363
ann[COLUMN_PREFERENCES_KEY] = PersistentDict(columnPreferences)
365
def getCurrentActivities(self, person):
366
worksheet = self.getCurrentWorksheet(person)
367
return self.getWorksheetActivities(worksheet)
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
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
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]
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))
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
411
class Gradebook(GradebookBase):
412
implements(interfaces.IGradebook)
413
adapts(interfaces.IWorksheet)
415
def __init__(self, context):
416
super(Gradebook, self).__init__(context)
417
# To make URL creation happy
418
self.__name__ = 'gradebook'
421
class MyGrades(GradebookBase):
422
implements(interfaces.IMyGrades)
423
adapts(interfaces.IWorksheet)
425
def __init__(self, context):
426
super(MyGrades, self).__init__(context)
427
# To make URL creation happy
428
self.__name__ = 'mygrades'
431
class StudentGradebook(object):
432
"""Adapter of student and gradebook used for grading one student at a
434
implements(interfaces.IStudentGradebook)
435
adapts(IBasicPerson, interfaces.IGradebook)
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)
445
class StudentGradebookFormAdapter(object):
446
"""Adapter used by grade student view to interact with student
448
implements(interfaces.IStudentGradebookForm)
449
adapts(interfaces.IStudentGradebook)
451
def __init__(self, context):
452
self.__dict__['context'] = context
454
def __setattr__(self, name, value):
455
gradebook = self.context.gradebook
456
student = self.context.student
457
activity = self.context.activities[name]
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)
465
score = activity.scoresystem.fromUnicode(value)
466
gradebook.evaluate(student, activity, score, evaluator)
467
except ScoreValidationError:
470
def __getattr__(self, name):
471
activity = self.context.activities[name]
472
value, ss = self.context.gradebook.getEvaluation(self.context.student,
474
if value is None or value is UNSCORED:
479
def getWorksheetSection(worksheet):
480
"""Adapt IWorksheet to ISection."""
481
return worksheet.__parent__.__parent__
484
def getGradebookSection(gradebook):
485
"""Adapt IGradebook to ISection."""
486
return course.interfaces.ISection(gradebook.context)
489
def getMyGradesSection(gradebook):
490
"""Adapt IMyGrades to ISection."""
491
return course.interfaces.ISection(gradebook.context)
494
class GradebookEditorsCrowd(ConfigurableCrowd):
495
setting_key = 'administration_can_grade_students'
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))