~schooltool-owners/schooltool.peas/packaging

« back to all changes in this revision

Viewing changes to src/schooltool/peas/assessment.py

  • Committer: Douglas Cerna
  • Date: 2015-02-17 08:43:21 UTC
  • mfrom: (9.1.28 schooltool.peas)
  • Revision ID: douglascerna@yahoo.com-20150217084321-l9mgb9oifyhjgs93
New release.

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) 2014 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, see <http://www.gnu.org/licenses/>.
 
17
#
 
18
"""
 
19
PEAS assessment initialization
 
20
"""
 
21
 
 
22
from collections import OrderedDict
 
23
from decimal import Decimal
 
24
 
 
25
from persistent import Persistent
 
26
from zope.annotation.interfaces import IAnnotations
 
27
from zope.cachedescriptors.property import Lazy
 
28
from zope.component import adapts
 
29
from zope.component import adapter
 
30
from zope.container.contained import Contained
 
31
from zope.interface import Interface
 
32
from zope.interface import implements
 
33
from zope.interface import implementer
 
34
from zope.lifecycleevent.interfaces import IObjectAddedEvent
 
35
from zope.proxy import sameProxiedObjects
 
36
from zope.schema import Bool
 
37
from zope.schema import Choice
 
38
from zope.schema import Int
 
39
from zope.schema import TextLine
 
40
 
 
41
from schooltool.app.app import StartUpBase
 
42
from schooltool.app.interfaces import ISchoolToolApplication
 
43
from schooltool.gradebook.activity import Activity
 
44
from schooltool.gradebook.activity import Worksheet
 
45
from schooltool.gradebook.interfaces import IActivities
 
46
from schooltool.gradebook.interfaces import ICategoryContainer
 
47
from schooltool.requirement.interfaces import IScoreSystemContainer
 
48
from schooltool.requirement.scoresystem import CustomScoreSystem
 
49
from schooltool.requirement.scoresystem import GlobalDiscreteValuesScoreSystem
 
50
from schooltool.requirement.scoresystem import RangedValuesScoreSystem
 
51
from schooltool.schoolyear.interfaces import ISchoolYear
 
52
from schooltool.schoolyear.subscriber import ObjectEventAdapterSubscriber
 
53
from schooltool.term.interfaces import ITerm
 
54
from schooltool.course.interfaces import ISection
 
55
 
 
56
from schooltool.peas import PeasMessage as _
 
57
 
 
58
 
 
59
ASSESSMENT_PREFERENCES_KEY = 'schooltool.peas.assessment_preferences'
 
60
PEAS_SCORESYSTEM_KEY = 'schooltool.peas.scoresystem'
 
61
MOCK_SHEET_ID = u'mock'
 
62
MOCK_OTHER_SHEET_ID = u'mock-other'
 
63
TARGET_SHEET_ID = u'target'
 
64
UCE_SHEET_ID = u'uce'
 
65
ASSESSMENT_SHEETS = OrderedDict([
 
66
    (u'bot',              {'title': u'Beginning of Term', 'editable': True}),
 
67
    (u'mot',              {'title': u'Middle of Term',    'editable': True}),
 
68
    (u'eot',              {'title': u'End of Term',       'editable': True}),
 
69
    (MOCK_SHEET_ID,       {'title': u'MOCK - PEAS',       'editable': False}),
 
70
    (MOCK_OTHER_SHEET_ID, {'title': u'MOCK - PEAS',       'editable': False}),
 
71
    (TARGET_SHEET_ID,     {'title': u'Target',            'editable': False}),
 
72
    (UCE_SHEET_ID,        {'title': u'UCE',               'editable': False}),
 
73
])
 
74
 
 
75
 
 
76
class IAssessmentPreferences(Interface):
 
77
 
 
78
    bot = Bool(title=_(u'Beginning of Term'), default=False, required=False)
 
79
    mot = Bool(title=_(u'Middle of Term'), default=False, required=False)
 
80
    eot = Bool(title=_(u'End of Term'), default=False, required=False)
 
81
    mock = Bool(title=_(u'MOCK - PEAS'), default=False, required=False)
 
82
    mock_term = Choice(
 
83
        title=_(u'Term for deploying the MOCK - PEAS sheet'),
 
84
        source='schooltool.peas.schoolyear_terms',
 
85
        required=False)
 
86
    mock_other = Bool(title=_(u'MOCK - Other'), default=False, required=False)
 
87
    mock_other_term = Choice(
 
88
        title=_(u'Term for deploying the MOCK - Other sheet'),
 
89
        source='schooltool.peas.schoolyear_terms',
 
90
        required=False)
 
91
 
 
92
 
 
93
class AssessmentPreferences(Persistent, Contained):
 
94
 
 
95
    implements(IAssessmentPreferences)
 
96
 
 
97
    bot = None
 
98
    mot = None
 
99
    eot = None
 
100
    mock = None
 
101
    mock_term = None
 
102
    mock_other = None
 
103
    mock_other_term = None
 
104
 
 
105
 
 
106
@adapter(IAssessmentPreferences)
 
107
@implementer(ISchoolYear)
 
108
def getAssessmentPreferencesSchoolYear(preferences):
 
109
    return preferences.__parent__
 
110
 
 
111
 
 
112
def getAssessmentPreferences(schoolyear):
 
113
    annotations = IAnnotations(schoolyear)
 
114
    name = 'assessment_preferences'
 
115
    try:
 
116
        return annotations[ASSESSMENT_PREFERENCES_KEY]
 
117
    except KeyError:
 
118
        annotations[ASSESSMENT_PREFERENCES_KEY] = AssessmentPreferences()
 
119
        annotations[ASSESSMENT_PREFERENCES_KEY].__name__ = name
 
120
        annotations[ASSESSMENT_PREFERENCES_KEY].__parent__ = schoolyear
 
121
        return annotations[ASSESSMENT_PREFERENCES_KEY]
 
122
 
 
123
 
 
124
def default_deploy_term(schoolyear):
 
125
    terms = sorted(schoolyear.values(), key=lambda term: term.first)
 
126
    if terms:
 
127
        return terms[0]
 
128
 
 
129
 
 
130
def get_peas_scoresystem():
 
131
    app = ISchoolToolApplication(None)
 
132
    scoresystems = IScoreSystemContainer(app)
 
133
    return scoresystems[PEAS_SCORESYSTEM_KEY]
 
134
 
 
135
 
 
136
class AssessmentSheetDeployBase(object):
 
137
 
 
138
    @Lazy
 
139
    def default_deploy_term(self):
 
140
        return default_deploy_term(self.schoolyear)
 
141
 
 
142
    def get_mock_term(self):
 
143
        if self.preferences.mock_term is not None:
 
144
            return self.preferences.mock_term
 
145
        # deploy mock sheet to second term automatically
 
146
        return self.default_deploy_term
 
147
 
 
148
    def get_mock_other_term(self):
 
149
        if self.preferences.mock_other_term is not None:
 
150
            return self.preferences.mock_other_term
 
151
        # deploy mock sheet to second term automatically
 
152
        return self.default_deploy_term
 
153
 
 
154
    def get_target_term(self):
 
155
        return self.default_deploy_term
 
156
 
 
157
    @Lazy
 
158
    def preferences(self):
 
159
        return IAssessmentPreferences(self.schoolyear)
 
160
 
 
161
    @Lazy
 
162
    def peas_scoresystem(self):
 
163
        return get_peas_scoresystem()
 
164
 
 
165
    @Lazy
 
166
    def ranged_scoresystem(self):
 
167
        return RangedValuesScoreSystem(
 
168
            u'generated', min=0, max=100)
 
169
 
 
170
    @Lazy
 
171
    def default_category(self):
 
172
        app = ISchoolToolApplication(None)
 
173
        return ICategoryContainer(app).default_key
 
174
 
 
175
    def deploy_assessment_sheets(self, section):
 
176
        activities = IActivities(section)
 
177
        term = ITerm(section)
 
178
        for sheet, sheet_data in ASSESSMENT_SHEETS.items():
 
179
            if not sheet_data['editable']:
 
180
                continue
 
181
            title = sheet_data['title']
 
182
            enabled = getattr(self.preferences, sheet, False)
 
183
            if enabled:
 
184
                if sheet in activities:
 
185
                    if activities[sheet].hidden:
 
186
                        activities[sheet].hidden = False
 
187
                        activities[sheet].deployed = False
 
188
                    continue
 
189
                activities[sheet] = Worksheet(title)
 
190
            elif sheet in activities:
 
191
                ws = activities[sheet]
 
192
                ws.hidden = True
 
193
                ws.deployed = True
 
194
        if self.preferences.mock:
 
195
            if sameProxiedObjects(term, self.get_mock_term()):
 
196
                if MOCK_SHEET_ID not in activities:
 
197
                    title = ASSESSMENT_SHEETS[MOCK_SHEET_ID]['title']
 
198
                    activities[MOCK_SHEET_ID] = Worksheet(title)
 
199
                elif activities[MOCK_SHEET_ID].hidden:
 
200
                    activities[MOCK_SHEET_ID].hidden = False
 
201
                    activities[MOCK_SHEET_ID].deployed = False
 
202
        elif MOCK_SHEET_ID in activities:
 
203
            ws = activities[MOCK_SHEET_ID]
 
204
            ws.hidden = True
 
205
            ws.deployed = True
 
206
        if self.preferences.mock_other:
 
207
            if sameProxiedObjects(term, self.get_mock_other_term()):
 
208
                if MOCK_OTHER_SHEET_ID not in activities:
 
209
                    title = ASSESSMENT_SHEETS[MOCK_OTHER_SHEET_ID]['title']
 
210
                    activities[MOCK_OTHER_SHEET_ID] = Worksheet(title)
 
211
                elif activities[MOCK_OTHER_SHEET_ID].hidden:
 
212
                    activities[MOCK_OTHER_SHEET_ID].hidden = False
 
213
                    activities[MOCK_OTHER_SHEET_ID].deployed = False
 
214
        elif MOCK_OTHER_SHEET_ID in activities:
 
215
            ws = activities[MOCK_OTHER_SHEET_ID]
 
216
            ws.hidden = True
 
217
            ws.deployed = True
 
218
        if sameProxiedObjects(term, self.get_target_term()):
 
219
            if TARGET_SHEET_ID not in activities:
 
220
                title = ASSESSMENT_SHEETS[TARGET_SHEET_ID]['title']
 
221
                ws = Worksheet(title)
 
222
                ws.deployed = True
 
223
                activities[TARGET_SHEET_ID] = ws
 
224
                activity_title = title + ' (%)'
 
225
                activity = Activity(
 
226
                    activity_title, self.default_category,
 
227
                    self.ranged_scoresystem, None, activity_title)
 
228
                ws[TARGET_SHEET_ID+'-ranged'] = activity
 
229
                activity_title = title + ' (1-9)'
 
230
                activity = Activity(
 
231
                    activity_title, self.default_category,
 
232
                    self.peas_scoresystem, None, activity_title)
 
233
                ws[TARGET_SHEET_ID] = activity
 
234
            elif activities[TARGET_SHEET_ID].hidden:
 
235
                activities[TARGET_SHEET_ID].hidden = False
 
236
            if UCE_SHEET_ID not in activities:
 
237
                title = ASSESSMENT_SHEETS[UCE_SHEET_ID]['title']
 
238
                ws = Worksheet(title)
 
239
                ws.deployed = True
 
240
                ws.hidden = True
 
241
                activities[UCE_SHEET_ID] = ws
 
242
                activity = Activity(
 
243
                    title, self.default_category,
 
244
                    self.peas_scoresystem, None, title)
 
245
                ws[UCE_SHEET_ID] = activity
 
246
        else:
 
247
            if TARGET_SHEET_ID in activities:
 
248
                ws = activities[TARGET_SHEET_ID]
 
249
                ws.hidden = True
 
250
                ws.deployed = True
 
251
 
 
252
 
 
253
class CreateAssessmentSheetsSubscriber(ObjectEventAdapterSubscriber,
 
254
                                       AssessmentSheetDeployBase):
 
255
 
 
256
    adapts(IObjectAddedEvent, ISection)
 
257
 
 
258
    @Lazy
 
259
    def schoolyear(self):
 
260
        return ISchoolYear(self.object)
 
261
 
 
262
    def __call__(self):
 
263
        self.deploy_assessment_sheets(self.object)
 
264
 
 
265
 
 
266
PEASScoreSystem = GlobalDiscreteValuesScoreSystem(
 
267
    'PEAS Score System',
 
268
    _('PEAS Score System'), None,
 
269
    [
 
270
        ('1', u'D1', Decimal(1), Decimal(80)),
 
271
        ('2', u'D2', Decimal(2), Decimal(75)),
 
272
        ('3', u'C3', Decimal(3), Decimal(70)),
 
273
        ('4', u'C4', Decimal(4), Decimal(65)),
 
274
        ('5', u'C5', Decimal(5), Decimal(60)),
 
275
        ('6', u'C6', Decimal(6), Decimal(50)),
 
276
        ('7', u'P7', Decimal(7), Decimal(45)),
 
277
        ('8', u'P8', Decimal(8), Decimal(40)),
 
278
        ('9', u'F9', Decimal(9), Decimal(0)),
 
279
    ],
 
280
    '1', '8', True)
 
281
 
 
282
 
 
283
class PeasScoreSystemAppStartup(StartUpBase):
 
284
 
 
285
    after = ('schooltool.requirement.scoresystem',)
 
286
 
 
287
    def __call__(self):
 
288
        container = IScoreSystemContainer(self.app)
 
289
        if PEAS_SCORESYSTEM_KEY not in container:
 
290
            ss = PEASScoreSystem
 
291
            peas_scoresystem = CustomScoreSystem(ss.title, ss.description,
 
292
                                                 ss.scores, ss._bestScore,
 
293
                                                 ss._minPassingScore,
 
294
                                                 ss._isMaxPassingScore)
 
295
            container[PEAS_SCORESYSTEM_KEY] = peas_scoresystem
 
296
 
 
297
 
 
298
HUMANITIES = ['208', '223', '225', '241', '273']
 
299
SCIENCE = ['535', '545', '553']
 
300
ENGLISH = ['112']
 
301
MATHEMATICS = ['456']
 
302
CREDIT_LEVEL_MIN_SCORE = 6
 
303
 
 
304
 
 
305
def find_division(items):
 
306
    ss = get_peas_scoresystem()
 
307
    passed = {}
 
308
    credit = {}
 
309
    for code, grade in items:
 
310
        if ss.isPassingScore(grade):
 
311
            decimal_value = ss.scoresDict[grade]
 
312
            passed[code] = decimal_value
 
313
            if ss.scoresDict.get(grade) <= CREDIT_LEVEL_MIN_SCORE:
 
314
                credit[code] = decimal_value
 
315
    if is_division_1(passed, credit):
 
316
        return 1
 
317
    elif is_division_2(passed, credit):
 
318
        return 2
 
319
    elif is_division_3(passed, credit):
 
320
        return 3
 
321
    elif is_division_4(passed, credit):
 
322
        return 4
 
323
    elif is_division_9(passed, credit):
 
324
        return 9
 
325
    elif not items:
 
326
        return 0
 
327
    return 7
 
328
 
 
329
 
 
330
def best8(passed):
 
331
    return sum(sorted(passed.values())[:8])
 
332
 
 
333
 
 
334
# XXX: use set intersection
 
335
def is_division_1(passed, credit):
 
336
    """
 
337
    Pass a minimum of eight subjects
 
338
    which must include English Language (with credit),
 
339
    a Humanity subject,
 
340
    mathematics and,
 
341
    a science subject.
 
342
    At least seven of the subjects must be a credit level or better.
 
343
    The aggregate for the best eight done subjects must not exceed 32
 
344
    """
 
345
    return (
 
346
        len(passed) >= 8 and
 
347
        [code for code in credit.keys() if code in ENGLISH] and
 
348
        [code for code in passed.keys() if code in HUMANITIES] and
 
349
        [code for code in passed.keys() if code in MATHEMATICS] and
 
350
        [code for code in passed.keys() if code in SCIENCE] and
 
351
        len(credit) >= 7 and
 
352
        best8(passed) <= 32
 
353
    )
 
354
 
 
355
 
 
356
def is_division_2(passed, credit):
 
357
    """
 
358
    Pass a minimum of eight subjects
 
359
    including English Language.
 
360
    Six of the subjects must be at a credit level or better.
 
361
    The aggregate for the best eight done subjects must not exceed 45.
 
362
    """
 
363
    return (
 
364
        len(passed) >= 8 and
 
365
        [code for code in passed.keys() if code in ENGLISH] and
 
366
        len(credit) >= 6 and
 
367
        best8(passed) <= 45
 
368
    )
 
369
 
 
370
 
 
371
def is_division_3(passed, credit):
 
372
    """
 
373
    Either
 
374
    (i) Pass a minimum of eight subjects (with at least 3 credits or better)
 
375
    OR (i) Pass a minimum of seven subjects (with at least 4 credits or better)
 
376
    OR (ii) Pass a minimum of five subjects with credits or better. 
 
377
    The aggregate for the best done subjects must not exceed 58.
 
378
    """
 
379
    return (
 
380
        (
 
381
            (len(passed) >= 8 and len(credit) >= 3) or
 
382
            (len(passed) >= 7 and len(credit) >= 4) or
 
383
            (len(passed) >= 5 and credit)
 
384
        ) and
 
385
        best8(passed) <= 58
 
386
    )
 
387
 
 
388
 
 
389
def is_division_4(passed, credit):
 
390
    """
 
391
    Either
 
392
    (i) Pass at least one subject with credit or better 
 
393
    OR (ii) Pass at least two subjects with pass 7 
 
394
    OR (iii) Pass at least three subjects with pass 8 or better
 
395
    """
 
396
    pass7 = [v for v in passed.values() if v == 7]
 
397
    pass8 = [v for v in passed.values() if v == 8]
 
398
    return (
 
399
        (
 
400
            credit or
 
401
            len(pass7) >= 2 or
 
402
            len(pass8) >= 3
 
403
        ) and
 
404
        best8(passed) <= 69
 
405
    )
 
406
 
 
407
 
 
408
def is_division_9(passed, credit):
 
409
    return best8(passed) <= 72
 
410
 
 
411
 
 
412
class IUCEResult(Interface):
 
413
 
 
414
    aggregate = Int(
 
415
        title=_(u'Aggregate'),
 
416
        required=False)
 
417
 
 
418
    division = TextLine(
 
419
        title=_(u'Division'),
 
420
        required=False)
 
421
 
 
422
 
 
423
class UCEResult(Persistent, Contained):
 
424
 
 
425
    aggregate = None
 
426
    division = None
 
427
 
 
428
 
 
429
def getUCEResult(person):
 
430
    annotations = IAnnotations(person)
 
431
    name = 'uce_result'
 
432
    try:
 
433
        return annotations[name]
 
434
    except KeyError:
 
435
        annotations[name] = UCEResult()
 
436
        annotations[name].__name__ = name
 
437
        annotations[name].__parent__ = person
 
438
        return annotations[name]