Merge lp:~aelkner/schooltool/schooltool.gradebook_column_preferences into lp:~schooltool-owners/schooltool/schooltool.gradebook

Proposed by Alan Elkner
Status: Merged
Merge reported by: Justas Sadzevičius
Merged at revision: not available
Proposed branch: lp:~aelkner/schooltool/schooltool.gradebook_column_preferences
Merge into: lp:~schooltool-owners/schooltool/schooltool.gradebook
Diff against target: None lines
To merge this branch: bzr merge lp:~aelkner/schooltool/schooltool.gradebook_column_preferences
Reviewer Review Type Date Requested Status
Justas Sadzevičius (community) Approve
Review via email: mp+7191@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Justas Sadzevičius (justas.sadzevicius) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/schooltool/gradebook/browser/README.txt'
2--- src/schooltool/gradebook/browser/README.txt 2009-05-08 01:30:55 +0000
3+++ src/schooltool/gradebook/browser/README.txt 2009-06-01 21:32:36 +0000
4@@ -7,8 +7,8 @@
5 SchoolTool setup is the configuration of the categories. So let's log in as a
6 manager:
7
8- >>> from schooltool.app.browser.ftests import setup
9- >>> manager = setup.logInManager()
10+ >>> from schooltool.app.browser.ftests import setup
11+ >>> manager = setup.logIn('manager', 'schooltool')
12
13
14 Initial School Setup
15@@ -557,6 +557,74 @@
16 '62'
17
18
19+Column Preferences
20+------------------
21+
22+Teachers may want to hide or change the label of the summary columns or, in
23+the case of the average column, they may want to choose a score system to
24+be used in converting the average to a discrete value. To support this, we
25+provide the column preferences view.
26+
27+First we will add a custom score system which we will use as a column
28+preference.
29+
30+ >>> manager.getLink('Manage').click()
31+ >>> manager.getLink('Score Systems').click()
32+ >>> manager.getLink('Add Score System').click()
33+ >>> url = manager.url + '?form-submitted&SAVE&title=Good/Bad'
34+ >>> url = url + '&displayed1=G&value1=1&percent1=60'
35+ >>> url = url + '&displayed2=B&value2=0&percent2=0'
36+ >>> manager.open(url)
37+
38+We'll start by calling up the current column preferences and note that there
39+are none set yet.
40+
41+ >>> stephan.getLink('Return to Gradebook').click()
42+ >>> stephan.getLink('Column Preferences').click()
43+ >>> analyze.printQuery("id('content-body')/form/table//input", stephan.contents)
44+ <input type="checkbox" name="hide_total" />
45+ <input type="text" name="label_total" value="" />
46+ <input type="checkbox" name="hide_average" />
47+ <input type="text" name="label_average" value="" />
48+
49+ >>> analyze.printQuery("id('content-body')/form/table//option", stephan.contents)
50+ <option selected="selected" value="">-- No score system --</option>
51+ <option value="extended-letter-grade">Extended Letter Grade</option>
52+ <option value="goodbad">Good/Bad</option>
53+ <option value="letter-grade">Letter Grade</option>
54+ <option value="passfail">Pass/Fail</option>
55+
56+Now we'll set all of the preferences to something and test that the changes
57+were saved.
58+
59+ >>> url = stephan.url + '?form-submitted&UPDATE_SUBMIT'
60+ >>> url += '&hide_total=on&label_total=Summe'
61+ >>> url += '&hide_average=on&label_average=Durchschnitt'
62+ >>> url += '&scoresystem_average=goodbad'
63+ >>> stephan.open(url)
64+
65+ >>> stephan.getLink('Column Preferences').click()
66+ >>> analyze.printQuery("id('content-body')/form/table//input", stephan.contents)
67+ <input type="checkbox" checked="checked" name="hide_total" />
68+ <input type="text" name="label_total" value="Summe" />
69+ <input type="checkbox" checked="checked" name="hide_average" />
70+ <input type="text" name="label_average" value="Durchschnitt" />
71+
72+ >>> analyze.printQuery("id('content-body')/form/table//option", stephan.contents)
73+ <option value="">-- No score system --</option>
74+ <option value="extended-letter-grade">Extended Letter Grade</option>
75+ <option selected="selected" value="goodbad">Good/Bad</option>
76+ <option value="letter-grade">Letter Grade</option>
77+ <option value="passfail">Pass/Fail</option>
78+
79+Finally, we will reset the preferences to none so that the rest of the tests
80+pass.
81+
82+ >>> url = stephan.url + '?form-submitted&UPDATE_SUBMIT'
83+ >>> url += '&scoresystem_average='
84+ >>> stephan.open(url)
85+
86+
87 My Grades
88 ---------
89
90@@ -575,7 +643,7 @@
91 True
92 >>> 'HW 2' in claudia.contents or 'Final' in claudia.contents
93 False
94- >>> claudia.contents.find('Current Grade: 86%') \
95+ >>> claudia.contents.find('Ave.: 86%') \
96 ... < claudia.contents.find('HW 1') \
97 ... < claudia.contents.find('Quiz') \
98 ... < claudia.contents.find('86 / 100')
99@@ -737,12 +805,12 @@
100 >>> claudia.open('http://localhost/schoolyears/2007/winter/sections/1/mygrades')
101 >>> print claudia.contents
102 <BLANKLINE>
103- ... Current Grade: 86%...
104+ ... Ave.: 86%...
105 >>> tom = setup.logIn('tom', 'pwd')
106 >>> tom.open('http://localhost/schoolyears/2007/winter/sections/1/mygrades')
107 >>> print tom.contents
108 <BLANKLINE>
109- ...Current Grade: 88%...
110+ ...Ave.: 88%...
111
112 Students should not be able to view a teacher's gradebook.
113
114
115=== modified file 'src/schooltool/gradebook/browser/configure.zcml'
116--- src/schooltool/gradebook/browser/configure.zcml 2009-05-08 01:30:55 +0000
117+++ src/schooltool/gradebook/browser/configure.zcml 2009-06-08 01:43:13 +0000
118@@ -352,6 +352,13 @@
119 action="../weights.html"
120 permission="schooltool.view"
121 />
122+ <menuItem
123+ menu="schooltool_actions"
124+ title="Column Preferences"
125+ for="..interfaces.IGradebook"
126+ action="column_preferences.html"
127+ permission="schooltool.view"
128+ />
129
130 <!-- Special navagation viewlet for update linked activity grades action -->
131 <navigationViewlet
132@@ -420,9 +427,15 @@
133 class=".gradebook.UpdateLinkedActivityGrades"
134 permission="schooltool.edit"
135 />
136+ <page
137+ name="column_preferences.html"
138+ for="..interfaces.IGradebook"
139+ class=".gradebook.GradebookColumnPreferences"
140+ template="gradebook_column_preferences.pt"
141+ permission="schooltool.view"
142+ />
143
144 <!-- Terms -->
145-
146 <zope:adapter
147 for="schooltool.gradebook.interfaces.IExternalActivitiesSource
148 zope.publisher.interfaces.browser.IBrowserRequest"
149@@ -488,7 +501,7 @@
150 permission="schooltool.edit"
151 />
152
153- <!-- Views for IBasicPerson-->
154+ <!-- Views for IBasicPerson -->
155 <page
156 name="print_report_card.html"
157 for="schooltool.person.interfaces.IPerson"
158@@ -513,6 +526,15 @@
159 permission="schooltool.edit"
160 />
161
162+ <!-- Views for ISchoolToolApplication -->
163+ <page
164+ name="no_current_term.html"
165+ for="schooltool.app.interfaces.ISchoolToolApplication"
166+ class=".gradebook.NoCurrentTerm"
167+ template="no_current_term.pt"
168+ permission="schooltool.view"
169+ />
170+
171 <zope:adapter
172 factory=".report_card.ExistingScoresSystem" />
173
174
175=== modified file 'src/schooltool/gradebook/browser/gradebook.css'
176--- src/schooltool/gradebook/browser/gradebook.css 2009-05-08 01:30:55 +0000
177+++ src/schooltool/gradebook/browser/gradebook.css 2009-06-01 21:32:36 +0000
178@@ -148,3 +148,7 @@
179 border: solid 1px black;
180 }
181
182+.bold {
183+ font-weight: bold;
184+}
185+
186
187=== modified file 'src/schooltool/gradebook/browser/gradebook.py'
188--- src/schooltool/gradebook/browser/gradebook.py 2009-05-11 13:16:00 +0000
189+++ src/schooltool/gradebook/browser/gradebook.py 2009-06-08 01:43:13 +0000
190@@ -20,38 +20,71 @@
191 Gradebook Views
192 """
193
194-from schooltool.course.interfaces import ILearner
195-from schooltool.course.interfaces import IInstructor
196 __docformat__ = 'reStructuredText'
197-import zope.schema
198+
199+import datetime
200+import decimal
201+
202+from zope.app.keyreference.interfaces import IKeyReference
203+from zope.component import queryUtility
204+from zope.publisher.browser import BrowserView
205+from zope.schema import ValidationError
206+from zope.schema.interfaces import IVocabularyFactory
207 from zope.security import proxy
208+from zope.traversing.api import getName
209 from zope.traversing.browser.absoluteurl import absoluteURL
210-from zope.app.keyreference.interfaces import IKeyReference
211 from zope.viewlet import viewlet
212-from zope.traversing.api import getName
213-from zope.publisher.browser import BrowserView
214
215 from schooltool.app import app
216 from schooltool.app.interfaces import ISchoolToolApplication
217 from schooltool.course.interfaces import ISection
218+from schooltool.course.interfaces import ILearner, IInstructor
219 from schooltool.gradebook import interfaces
220 from schooltool.gradebook.activity import ensureAtLeastOneWorksheet
221 from schooltool.person.interfaces import IPerson
222 from schooltool.requirement.scoresystem import UNSCORED
223-from schooltool.common import SchoolToolMessage as _
224 from schooltool.requirement.interfaces import IValuesScoreSystem
225 from schooltool.requirement.interfaces import IDiscreteValuesScoreSystem
226 from schooltool.requirement.interfaces import IRangedValuesScoreSystem
227 from schooltool.term.interfaces import ITerm
228
229-import datetime
230-import decimal
231+from schooltool.common import SchoolToolMessage as _
232+
233
234 GradebookCSSViewlet = viewlet.CSSViewlet("gradebook.css")
235
236 DISCRETE_SCORE_SYSTEM = 'd'
237 RANGED_SCORE_SYSTEM = 'r'
238
239+column_keys = [('total', _("Total")), ('average', _("Ave."))]
240+
241+
242+def escName(name):
243+ """converts title-based scoresystem name to querystring format"""
244+ chars = [c for c in name.lower() if c.isalnum() or c == ' ']
245+ return u''.join(chars).replace(' ', '-')
246+
247+
248+def getScoreSystemFromEscName(name):
249+ """converts escaped scoresystem title to scoresystem"""
250+ factory = queryUtility(IVocabularyFactory,
251+ 'schooltool.requirement.discretescoresystems')
252+ vocab = factory(None)
253+ for term in vocab:
254+ if name == escName(term.token):
255+ return term.value
256+ return None
257+
258+
259+def convertAverage(average, scoresystem):
260+ """converts average to display value of the given scoresystem"""
261+ if scoresystem is None:
262+ return '%s%%' % average
263+ for score in scoresystem.scores:
264+ if average >= score[2]:
265+ return score[0]
266+ raise ValueError
267+
268
269 class GradebookStartup(object):
270 """A view for entry into into the gradebook or mygrades views."""
271@@ -241,6 +274,37 @@
272 return True
273 return False
274
275+ def processColumnPreferences(self):
276+ gradebook = proxy.removeSecurityProxy(self.context)
277+ if self.isTeacher:
278+ person = self.person
279+ else:
280+ section = ISection(gradebook)
281+ instructors = list(section.instructors)
282+ if len(instructors) == 0:
283+ return {}
284+ person = instructors[0]
285+ columnPreferences = gradebook.getColumnPreferences(person)
286+ column_keys_dict = dict(column_keys)
287+ prefs = columnPreferences.get('total', {})
288+ self.total_hide = prefs.get('hide', False)
289+ self.total_label = prefs.get('label', '')
290+ if len(self.total_label) == 0:
291+ self.total_label = column_keys_dict['total']
292+ prefs = columnPreferences.get('average', {})
293+ self.average_hide = prefs.get('hide', False)
294+ self.average_label = prefs.get('label', '')
295+ if len(self.average_label) == 0:
296+ self.average_label = column_keys_dict['average']
297+ self.average_scoresystem = getScoreSystemFromEscName(
298+ prefs.get('scoresystem', ''))
299+ self.apply_all_colspan = 1
300+ if not self.total_hide:
301+ self.apply_all_colspan += 1
302+ if not self.average_hide:
303+ self.apply_all_colspan += 1
304+
305+
306 class GradebookOverview(SectionFinder):
307 """Gradebook Overview/Table"""
308
309@@ -255,6 +319,9 @@
310 worksheet = gradebook.context
311 gradebook.setCurrentWorksheet(self.person, worksheet)
312
313+ """Retrieve column preferences."""
314+ self.processColumnPreferences()
315+
316 """Retrieve sorting information and store changes of it."""
317 if 'sort_by' in self.request:
318 sort_by = self.request['sort_by']
319@@ -297,7 +364,7 @@
320 try:
321 score = activity.scoresystem.fromUnicode(
322 self.request[cell_name])
323- except (zope.schema.ValidationError, ValueError):
324+ except (ValidationError, ValueError):
325 self.message = _(
326 'The grade $value for activity $name is not valid.',
327 mapping={'value': self.request[cell_name],
328@@ -387,6 +454,8 @@
329 total, average = gradebook.getWorksheetTotalAverage(worksheet,
330 student)
331
332+ average = convertAverage(average, self.average_scoresystem)
333+
334 rows.append(
335 {'student': {'title': student.title,
336 'id': student.username,
337@@ -518,7 +587,7 @@
338 try:
339 score = activity.scoresystem.fromUnicode(
340 self.request[id])
341- except (zope.schema.ValidationError, ValueError):
342+ except (ValidationError, ValueError):
343 self.message = _(
344 'The grade $value for $name is not valid.',
345 mapping={'value': self.request[id],
346@@ -579,6 +648,9 @@
347 if self.handleSectionChange():
348 return
349
350+ """Retrieve column preferences."""
351+ self.processColumnPreferences()
352+
353 self.table = []
354 total = 0
355 count = 0
356@@ -602,8 +674,10 @@
357
358 self.table.append({'activity': activity.title,
359 'grade': grade})
360+
361 if count:
362- self.average = int((float(100 * total) / float(count)) + 0.5)
363+ average = int((float(100 * total) / float(count)) + 0.5)
364+ self.average = convertAverage(average, self.average_scoresystem)
365 else:
366 self.average = None
367
368@@ -638,3 +712,80 @@
369 next_url = absoluteURL(self.context.__parent__, self.request) + \
370 '/gradebook'
371 self.request.response.redirect(next_url)
372+
373+
374+class GradebookColumnPreferences(BrowserView):
375+ """A view for editing a teacher's gradebook column preferences."""
376+
377+ def update(self):
378+ self.person = IPerson(self.request.principal)
379+ gradebook = proxy.removeSecurityProxy(self.context)
380+
381+ if 'UPDATE_SUBMIT' in self.request:
382+ columnPreferences = gradebook.getColumnPreferences(self.person)
383+ for key, name in column_keys:
384+ prefs = columnPreferences.setdefault(key, {})
385+ if 'hide_' + key in self.request:
386+ prefs['hide'] = True
387+ else:
388+ prefs['hide'] = False
389+ if 'label_' + key in self.request:
390+ prefs['label'] = self.request['label_' + key]
391+ else:
392+ prefs['label'] = ''
393+ if key != 'total':
394+ prefs['scoresystem'] = self.request['scoresystem_' + key]
395+ gradebook.setColumnPreferences(self.person, columnPreferences)
396+
397+ if 'CANCEL' in self.request or 'UPDATE_SUBMIT' in self.request:
398+ self.request.response.redirect('index.html')
399+
400+ @property
401+ def columns(self):
402+ self.person = IPerson(self.request.principal)
403+ gradebook = proxy.removeSecurityProxy(self.context)
404+ results = []
405+ columnPreferences = gradebook.getColumnPreferences(self.person)
406+ for key, name in column_keys:
407+ prefs = columnPreferences.get(key, {})
408+ hide = prefs.get('hide', False)
409+ label = prefs.get('label', '')
410+ scoresystem = prefs.get('scoresystem', '')
411+ result = {
412+ 'name': name,
413+ 'hide_name': 'hide_' + key,
414+ 'hide_value': hide,
415+ 'label_name': 'label_' + key,
416+ 'label_value': label,
417+ 'scoresystem_name': 'scoresystem_' + key,
418+ 'scoresystem_value': scoresystem,
419+ }
420+ results.append(result)
421+ return results
422+
423+ @property
424+ def scoresystems(self):
425+ factory = queryUtility(IVocabularyFactory,
426+ 'schooltool.requirement.discretescoresystems')
427+ vocab = factory(None)
428+ result = {
429+ 'name': _('-- No score system --'),
430+ 'value': '',
431+ }
432+ results = [result]
433+ for term in vocab:
434+ result = {
435+ 'name': term.token,
436+ 'value': escName(term.token),
437+ }
438+ results.append(result)
439+ return results
440+
441+
442+class NoCurrentTerm(BrowserView):
443+ """A view for informing the user of the need to set up a schoolyear
444+ and at least one term."""
445+
446+ def update(self):
447+ pass
448+
449
450=== added file 'src/schooltool/gradebook/browser/gradebook_column_preferences.pt'
451--- src/schooltool/gradebook/browser/gradebook_column_preferences.pt 1970-01-01 00:00:00 +0000
452+++ src/schooltool/gradebook/browser/gradebook_column_preferences.pt 2009-05-14 10:35:19 +0000
453@@ -0,0 +1,67 @@
454+<tal:define define="dummy view/update"/>
455+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
456+<head>
457+ <title metal:fill-slot="title" i18n:translate="">Set Column Preferences</title>
458+</head>
459+<body>
460+
461+<h1 metal:fill-slot="content-header"
462+ i18n:translate="">Set Column Preferences</h1>
463+
464+<metal:block metal:fill-slot="body">
465+ <form method="post"
466+ tal:attributes="action string:${context/@@absolute_url}/column_preferences.html">
467+ <input type="hidden" name="form-submitted" value="" />
468+
469+ <table class="schooltool_gradebook">
470+ <tr>
471+ <th class="cell header fully_padded">Column Type</th>
472+ <th class="cell header fully_padded">Hide?</th>
473+ <th class="cell header fully_padded">Label</th>
474+ <th class="cell header fully_padded">Score System</th>
475+ </tr>
476+
477+ <tal:block repeat="column view/columns">
478+ <tr class="bordered">
479+ <td class="cell fully_padded">
480+ <div tal:content="column/name" />
481+ </td>
482+ <td class="cell fully_padded">
483+ <input type="checkbox"
484+ tal:attributes="name column/hide_name; checked column/hide_value" />
485+ </td>
486+ <td class="cell fully_padded">
487+ <input type="text"
488+ tal:attributes="name column/label_name; value column/label_value" />
489+ </td>
490+ <td class="cell fully_padded">
491+ <tal:block condition="python: column['hide_name'] != 'hide_total'">
492+ <select tal:attributes="id column/scoresystem_name; name column/scoresystem_name">
493+ <tal:block repeat="scoresystem view/scoresystems">
494+ <option tal:condition="python: scoresystem['value'] == column['scoresystem_value']"
495+ selected
496+ tal:attributes="value scoresystem/value"
497+ tal:content="scoresystem/name" />
498+ <option tal:condition="python: scoresystem['value'] != column['scoresystem_value']"
499+ tal:attributes="value scoresystem/value"
500+ tal:content="scoresystem/name" />
501+ </tal:block>
502+ </select>
503+ </tal:block>
504+ </td>
505+ </tr>
506+ </tal:block>
507+
508+ </table>
509+ <div style="height: 11px;"></div>
510+
511+ <div class="controls">
512+ <input type="submit" class="button-ok" name="UPDATE_SUBMIT" value="Update" />
513+ <input type="submit" class="button-cancel" name="CANCEL" value="Cancel" />
514+ </div>
515+ </form>
516+
517+</metal:block>
518+</body>
519+</html>
520+
521
522=== modified file 'src/schooltool/gradebook/browser/gradebook_overview.js.pt'
523--- src/schooltool/gradebook/browser/gradebook_overview.js.pt 2009-04-17 04:22:56 +0000
524+++ src/schooltool/gradebook/browser/gradebook_overview.js.pt 2009-04-30 19:20:05 +0000
525@@ -160,7 +160,6 @@
526 }
527 if (keynum < 48 || (keynum > 57 && keynum < 65) || (keynum > 90 && keynum < 97) || keynum > 122)
528 {
529- document.getElementById(name).select();
530 return true;
531 }
532
533
534=== modified file 'src/schooltool/gradebook/browser/gradebook_overview.pt'
535--- src/schooltool/gradebook/browser/gradebook_overview.pt 2009-04-29 00:39:08 +0000
536+++ src/schooltool/gradebook/browser/gradebook_overview.pt 2009-05-15 06:01:23 +0000
537@@ -114,7 +114,7 @@
538 <table class="schooltool_gradebook">
539 <tr tal:condition="python:len(view.descriptions) > 0">
540 <td id="description" class="description"
541- tal:attributes="colspan python:len(view.descriptions) + 3" />
542+ tal:attributes="colspan python:len(view.descriptions) + view.apply_all_colspan" />
543 </tr>
544
545 <tr>
546@@ -122,11 +122,11 @@
547 <div i18n:translate="">Name</div>
548 <a href="?sort_by=student" i18n:translate="">(sort)</a>
549 </th>
550- <th class="cell header">
551- <div i18n:translate="">Total</div>
552+ <th tal:condition="not: view/total_hide" class="cell padded header">
553+ <div tal:content="view/total_label" />
554 </th>
555- <th class="cell header">
556- <div i18n:translate="">Ave.</div>
557+ <th tal:condition="not: view/average_hide" class="cell padded header">
558+ <div tal:content="view/average_label" />
559 <a tal:condition="nothing" href="?sort_by=average" i18n:translate="">(sort)</a>
560 </th>
561 <th class="cell title header" tal:repeat="activity view/activities">
562@@ -152,7 +152,8 @@
563 </tr>
564
565 <tr>
566- <td colspan="3" class="cell padded fd_cell empty" i18n:translate="">Apply a grade for all students:</td>
567+ <td tal:attributes="colspan view/apply_all_colspan"
568+ class="cell padded fd_cell empty" i18n:translate="">Apply a grade for all students:</td>
569 <td class="cell padded fd_cell" tal:repeat="activity view/activities"
570 tal:attributes="id string:fd_${activity/hash}_cell">
571 <input type="text" size="4" class="data fd"
572@@ -178,14 +179,14 @@
573 Tom Hoffman
574 </a>
575 </td>
576- <td class="cell padded even">
577+ <td tal:condition="not: view/total_hide" class="cell padded even">
578 <tal:if condition="row/total|nothing">
579 <b><span tal:replace="row/total" /></b>
580 </tal:if>
581 </td>
582- <td class="cell padded even">
583+ <td tal:condition="not: view/average_hide" class="cell padded even">
584 <tal:if condition="row/average|nothing">
585- <b><span tal:replace="row/average" />%</b>
586+ <b><span tal:replace="row/average" /></b>
587 </tal:if>
588 </td>
589 <td class="cell even" tal:repeat="grade row/grades"
590@@ -207,14 +208,14 @@
591 Tom Hoffman
592 </a>
593 </td>
594- <td class="cell padded odd">
595+ <td tal:condition="not: view/total_hide" class="cell padded odd">
596 <tal:if condition="row/total|nothing">
597 <b><span tal:replace="row/total" /></b>
598 </tal:if>
599 </td>
600- <td class="cell padded odd">
601+ <td tal:condition="not: view/average_hide" class="cell padded odd">
602 <tal:if condition="row/average|nothing">
603- <b><span tal:replace="row/average" />%</b>
604+ <b><span tal:replace="row/average" /></b>
605 </tal:if>
606 </td>
607 <td class="cell odd" tal:repeat="grade row/grades"
608
609=== modified file 'src/schooltool/gradebook/browser/mygrades.pt'
610--- src/schooltool/gradebook/browser/mygrades.pt 2009-04-29 00:39:08 +0000
611+++ src/schooltool/gradebook/browser/mygrades.pt 2009-05-15 06:01:23 +0000
612@@ -72,10 +72,10 @@
613 </table>
614
615 <table class="schooltool_gradebook">
616- <tr>
617+ <tr tal:condition="not: view/average_hide">
618 <td colspan="2" class="description">
619 <div tal:condition="view/average"
620- tal:content="string: Current Grade: ${view/average}%">Average</div>
621+ tal:content="string: ${view/average_label}: ${view/average}">Average</div>
622 <div tal:condition="not: view/average">Nothing Graded</div>
623 </td>
624 </tr>
625
626=== added file 'src/schooltool/gradebook/browser/no_current_term.pt'
627--- src/schooltool/gradebook/browser/no_current_term.pt 1970-01-01 00:00:00 +0000
628+++ src/schooltool/gradebook/browser/no_current_term.pt 2009-06-08 01:43:13 +0000
629@@ -0,0 +1,16 @@
630+<tal:tag condition="view/update" />
631+<html metal:use-macro="context/@@standard_macros/page"
632+ i18n:domain="schooltool">
633+<head>
634+ <title metal:fill-slot="title" i18n:translate="">
635+ No Current Term
636+ </title>
637+</head>
638+<body>
639+<div metal:fill-slot="body">
640+ <h1>The operation you attempted cannot be completed because there are curently
641+ no terms set up in your SchoolTool instance. Please have a user with
642+ administation access set up at least one term and try the operation again.</h1>
643+</div>
644+</body>
645+</html>
646
647=== added file 'src/schooltool/gradebook/browser/no_current_term.txt'
648--- src/schooltool/gradebook/browser/no_current_term.txt 1970-01-01 00:00:00 +0000
649+++ src/schooltool/gradebook/browser/no_current_term.txt 2009-06-08 01:43:13 +0000
650@@ -0,0 +1,25 @@
651+===============
652+No Current Term
653+===============
654+
655+If a schooltool operation relies on there being at least one term set up, and
656+the administator has not gotten around to doing that, we need to fail
657+gracefully. That means giving the user an error message rather than crashing.
658+
659+Let's log in as manager and create a student.
660+
661+ >>> from schooltool.app.browser.ftests import setup
662+ >>> manager = Browser('manager', 'schooltool')
663+ >>> setup.addPerson('Student One', 'student1', 'pwd')
664+
665+We'll navigate to the student and request a report card, an operation that is
666+impossible without there being at least one term set up. We'll test that the
667+user gets redirected to the error page.
668+
669+ >>> manager.getLink('Manage').click()
670+ >>> manager.getLink('Persons').click()
671+ >>> manager.getLink('Student One').click()
672+ >>> manager.getLink('Print Report Card').click()
673+ >>> manager.url
674+ 'http://localhost/no_current_term.html'
675+
676
677=== modified file 'src/schooltool/gradebook/browser/pdf_views.py'
678--- src/schooltool/gradebook/browser/pdf_views.py 2009-05-11 13:08:19 +0000
679+++ src/schooltool/gradebook/browser/pdf_views.py 2009-06-08 01:43:13 +0000
680@@ -35,6 +35,7 @@
681 from zope.component import getUtility, queryAdapter
682 from zope.i18n import translate
683 from zope.publisher.browser import BrowserView
684+from zope.traversing.browser.absoluteurl import absoluteURL
685
686 from schooltool.app.interfaces import ISchoolToolApplication
687 from schooltool.app.browser import pdfcal
688@@ -256,6 +257,13 @@
689
690 def __call__(self):
691 """Return the PDF of a report card for each student."""
692+ current_term = getUtility(IDateManager).current_term
693+ if current_term is None:
694+ next_url = absoluteURL(ISchoolToolApplication(None), self.request)
695+ next_url += '/no_current_term.html'
696+ self.request.response.redirect(next_url)
697+ return
698+
699 if self.pdf_support_disabled:
700 return translate(self.pdf_disabled_text, context=self.request)
701
702
703=== modified file 'src/schooltool/gradebook/configure.zcml'
704--- src/schooltool/gradebook/configure.zcml 2009-04-17 13:26:14 +0000
705+++ src/schooltool/gradebook/configure.zcml 2009-06-05 00:11:04 +0000
706@@ -284,4 +284,11 @@
707 <adapter factory=".gradebook_init.GradebookAppStartup"
708 name="gradebook_startup_init" />
709
710+ <!-- generations -->
711+ <utility
712+ name="schooltool.gradebook"
713+ provides="zope.app.generations.interfaces.ISchemaManager"
714+ component=".generations.schemaManager"
715+ />
716+
717 </configure>
718
719=== added directory 'src/schooltool/gradebook/generations'
720=== added file 'src/schooltool/gradebook/generations/__init__.py'
721--- src/schooltool/gradebook/generations/__init__.py 1970-01-01 00:00:00 +0000
722+++ src/schooltool/gradebook/generations/__init__.py 2009-06-05 00:11:04 +0000
723@@ -0,0 +1,28 @@
724+#
725+# SchoolTool - common information systems platform for school administration
726+# Copyright (c) 2008 Shuttleworth Foundation
727+#
728+# This program is free software; you can redistribute it and/or modify
729+# it under the terms of the GNU General Public License as published by
730+# the Free Software Foundation; either version 2 of the License, or
731+# (at your option) any later version.
732+#
733+# This program is distributed in the hope that it will be useful,
734+# but WITHOUT ANY WARRANTY; without even the implied warranty of
735+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
736+# GNU General Public License for more details.
737+#
738+# You should have received a copy of the GNU General Public License
739+# along with this program; if not, write to the Free Software
740+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
741+#
742+"""
743+Generations for database version upgrades.
744+"""
745+
746+from zope.app.generations.generations import SchemaManager
747+
748+schemaManager = SchemaManager(
749+ minimum_generation=1,
750+ generation=1,
751+ package_name='schooltool.gradebook.generations')
752
753=== added file 'src/schooltool/gradebook/generations/evolve1.py'
754--- src/schooltool/gradebook/generations/evolve1.py 1970-01-01 00:00:00 +0000
755+++ src/schooltool/gradebook/generations/evolve1.py 2009-06-05 00:11:04 +0000
756@@ -0,0 +1,89 @@
757+#
758+# SchoolTool - common information systems platform for school administration
759+# Copyright (c) 2008 Shuttleworth Foundation
760+#
761+# This program is free software; you can redistribute it and/or modify
762+# it under the terms of the GNU General Public License as published by
763+# the Free Software Foundation; either version 2 of the License, or
764+# (at your option) any later version.
765+#
766+# This program is distributed in the hope that it will be useful,
767+# but WITHOUT ANY WARRANTY; without even the implied warranty of
768+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
769+# GNU General Public License for more details.
770+#
771+# You should have received a copy of the GNU General Public License
772+# along with this program; if not, write to the Free Software
773+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
774+#
775+"""
776+Evolve database to generation 1.
777+
778+Moves hard-coded score system utilities to the app site manager.
779+"""
780+
781+from zope.app.zopeappgenerations import getRootFolder
782+
783+from schooltool.app.interfaces import ISchoolToolApplication
784+from schooltool.course.interfaces import ISectionContainer
785+from schooltool.gradebook.interfaces import IActivities, IGradebookRoot
786+from schooltool.person.interfaces import IPersonContainer
787+from schooltool.schoolyear.interfaces import ISchoolYearContainer
788+from schooltool.requirement.interfaces import IScoreSystemsProxy
789+from schooltool.requirement.interfaces import IDiscreteValuesScoreSystem
790+from schooltool.requirement.interfaces import IEvaluations
791+from schooltool.requirement.scoresystem import CustomScoreSystem, PassFail
792+from schooltool.requirement.scoresystem import AmericanLetterScoreSystem
793+from schooltool.requirement.scoresystem import ExtendedAmericanLetterScoreSystem
794+
795+
796+def updateEvaluations(app, ss, custom_ss):
797+ """Update all evaluations using the hard-coded score system to use the
798+ newly created custom score system"""
799+
800+ for person in app['persons'].values():
801+ evaluations = IEvaluations(person)
802+ for evaluation in evaluations.values():
803+ if evaluation.scoreSystem == ss:
804+ evaluation.scoreSystem = custom_ss
805+ person._p_changed = True
806+
807+
808+def updateObjActivities(obj, activities, ss, custom_ss):
809+ """Update the obj activities using the hard-coded score system to use the
810+ newly created custom score system"""
811+
812+ for worksheet in activities.values():
813+ for activity in worksheet.values():
814+ if activity.scoresystem == ss:
815+ activity.scoresystem = custom_ss
816+ obj._p_changed = True
817+
818+
819+def updateAllActivities(app, ss, custom_ss):
820+ """Update all activities using the hard-coded score system to use the
821+ newly created custom score system"""
822+
823+ for sections in app['schooltool.course.section'].values():
824+ for section in sections.values():
825+ updateObjActivities(section, IActivities(section), ss, custom_ss)
826+
827+ root = IGradebookRoot(app)
828+ updateObjActivities(root, root.templates, ss, custom_ss)
829+ updateObjActivities(root, root.deployed, ss, custom_ss)
830+
831+
832+def evolve(context):
833+ """Migrates hard-coded discrete values score systems found in
834+ schooltool.requirement.scoresystem to the app site manager"""
835+
836+ app = getRootFolder(context)
837+ ssProxy = IScoreSystemsProxy(app)
838+ for ss in [PassFail, AmericanLetterScoreSystem,
839+ ExtendedAmericanLetterScoreSystem]:
840+ custom_ss = CustomScoreSystem(ss.title, ss.description, ss.scores,
841+ ss._bestScore, ss._minPassingScore)
842+ ssProxy.addScoreSystem(custom_ss)
843+ updateEvaluations(app, ss, custom_ss)
844+ updateAllActivities(app, ss, custom_ss)
845+
846
847=== added file 'src/schooltool/gradebook/generations/install.py'
848--- src/schooltool/gradebook/generations/install.py 1970-01-01 00:00:00 +0000
849+++ src/schooltool/gradebook/generations/install.py 2009-06-05 00:11:04 +0000
850@@ -0,0 +1,31 @@
851+#
852+# SchoolTool - common information systems platform for school administration
853+# Copyright (c) 2008 Shuttleworth Foundation
854+#
855+# This program is free software; you can redistribute it and/or modify
856+# it under the terms of the GNU General Public License as published by
857+# the Free Software Foundation; either version 2 of the License, or
858+# (at your option) any later version.
859+#
860+# This program is distributed in the hope that it will be useful,
861+# but WITHOUT ANY WARRANTY; without even the implied warranty of
862+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
863+# GNU General Public License for more details.
864+#
865+# You should have received a copy of the GNU General Public License
866+# along with this program; if not, write to the Free Software
867+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
868+#
869+"""
870+Initial deployment of generations to schooltool.gradebook.
871+
872+Manually evolves to generation 1. Done to avoid double deployment when
873+introducing generations.
874+"""
875+
876+import evolve1
877+
878+
879+def evolve(context):
880+ evolve1.evolve(context)
881+
882
883=== added directory 'src/schooltool/gradebook/generations/tests'
884=== added file 'src/schooltool/gradebook/generations/tests/__init__.py'
885--- src/schooltool/gradebook/generations/tests/__init__.py 1970-01-01 00:00:00 +0000
886+++ src/schooltool/gradebook/generations/tests/__init__.py 2009-06-05 00:11:04 +0000
887@@ -0,0 +1,72 @@
888+"""
889+Tests for generation scripts.
890+"""
891+
892+from persistent.interfaces import IPersistent
893+
894+from zope.app.keyreference.interfaces import IKeyReference
895+from zope.app.publication.zopepublication import ZopePublication
896+from zope.app.testing.setup import setUpAnnotations
897+from zope.component import provideAdapter
898+from zope.interface import implements
899+
900+from schooltool.app.app import SchoolToolApplication
901+from schooltool.app.interfaces import ISchoolToolApplication
902+from schooltool.course.interfaces import ISection
903+from schooltool.gradebook.activity import getSectionActivities
904+from schooltool.gradebook.interfaces import IGradebookRoot, IActivities
905+from schooltool.gradebook.gradebook_init import getGradebookRoot
906+from schooltool.requirement.evaluation import getEvaluations
907+from schooltool.requirement.interfaces import IEvaluations
908+from schooltool.requirement.interfaces import IHaveEvaluations
909+from schooltool.requirement.scoresystem import ScoreSystemsProxy
910+
911+
912+class ContextStub(object):
913+ """Stub for the context argument passed to evolve scripts.
914+
915+ >>> from zope.app.zopeappgenerations import getRootFolder
916+ >>> context = ContextStub()
917+ >>> getRootFolder(context) is context.root_folder
918+ True
919+ """
920+
921+ class ConnectionStub(object):
922+ def __init__(self, root_folder):
923+ self.root_folder = root_folder
924+ def root(self):
925+ return {ZopePublication.root_name: self.root_folder}
926+
927+ def __init__(self):
928+ self.root_folder = SchoolToolApplication()
929+ self.connection = self.ConnectionStub(self.root_folder)
930+
931+
932+_d = {}
933+
934+class StupidKeyReference(object):
935+ implements(IKeyReference)
936+ key_type_id = 'StupidKeyReference'
937+ def __init__(self, ob):
938+ global _d
939+ self.id = id(ob)
940+ _d[self.id] = ob
941+ def __call__(self):
942+ return _d[self.id]
943+ def __hash__(self):
944+ return self.id
945+ def __cmp__(self, other):
946+ return cmp(hash(self), hash(other))
947+
948+
949+def provideAdapters():
950+ setUpAnnotations()
951+ provideAdapter(StupidKeyReference, [IPersistent], IKeyReference)
952+ provideAdapter(ScoreSystemsProxy)
953+ provideAdapter(getGradebookRoot, adapts=(ISchoolToolApplication,),
954+ provides=IGradebookRoot)
955+ provideAdapter(getSectionActivities, adapts=(ISection,),
956+ provides=IActivities)
957+ provideAdapter(getEvaluations, adapts=(IHaveEvaluations,),
958+ provides=IEvaluations)
959+
960
961=== added file 'src/schooltool/gradebook/generations/tests/test_evolve1.py'
962--- src/schooltool/gradebook/generations/tests/test_evolve1.py 1970-01-01 00:00:00 +0000
963+++ src/schooltool/gradebook/generations/tests/test_evolve1.py 2009-06-05 00:11:04 +0000
964@@ -0,0 +1,140 @@
965+# coding=UTF8
966+#
967+# SchoolTool - common information systems platform for school administration
968+# Copyright (c) 2008 Shuttleworth Foundation
969+#
970+# This program is free software; you can redistribute it and/or modify
971+# it under the terms of the GNU General Public License as published by
972+# the Free Software Foundation; either version 2 of the License, or
973+# (at your option) any later version.
974+#
975+# This program is distributed in the hope that it will be useful,
976+# but WITHOUT ANY WARRANTY; without even the implied warranty of
977+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
978+# GNU General Public License for more details.
979+#
980+# You should have received a copy of the GNU General Public License
981+# along with this program; if not, write to the Free Software
982+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
983+#
984+"""
985+Unit tests for schooltool.gradebook.generations.evolve1
986+"""
987+
988+import unittest
989+
990+from zope.app.zopeappgenerations import getRootFolder
991+from zope.testing import doctest
992+
993+from schooltool.app.interfaces import ISchoolToolApplication
994+from schooltool.gradebook.generations.tests import ContextStub
995+from schooltool.gradebook.generations.tests import provideAdapters
996+from schooltool.gradebook.generations.evolve1 import evolve
997+from schooltool.requirement.interfaces import IHaveEvaluations
998+
999+
1000+def doctest_evolve1():
1001+ """Evolution to generation 1.
1002+
1003+ First, we'll set up the app object:
1004+
1005+ >>> provideAdapters()
1006+ >>> context = ContextStub()
1007+ >>> app = getRootFolder(context)
1008+
1009+ >>> from zope.app.component.site import LocalSiteManager
1010+ >>> app.setSiteManager(LocalSiteManager(app))
1011+
1012+ We'll set up our test with data that will be effected by running the
1013+ evolve script:
1014+
1015+ >>> from schooltool.person.person import PersonContainer, Person
1016+ >>> app['persons'] = PersonContainer()
1017+ >>> student = Person('student')
1018+ >>> app['persons']['student'] = student
1019+
1020+ >>> from schooltool.course.section import SectionContainerContainer
1021+ >>> from schooltool.course.section import SectionContainer, Section
1022+ >>> app['schooltool.course.section'] = SectionContainerContainer()
1023+ >>> app['schooltool.course.section']['1'] = SectionContainer()
1024+ >>> section = Section('section')
1025+ >>> app['schooltool.course.section']['1']['1'] = section
1026+
1027+ >>> from schooltool.gradebook.gradebook_init import setUpGradebookRoot
1028+ >>> from schooltool.gradebook.interfaces import IGradebookRoot
1029+ >>> setUpGradebookRoot(app)
1030+ >>> root = IGradebookRoot(app)
1031+
1032+ >>> from schooltool.requirement.scoresystem import PassFail
1033+ >>> from schooltool.requirement.scoresystem import AmericanLetterScoreSystem
1034+ >>> from schooltool.gradebook.activity import ReportWorksheet
1035+ >>> from schooltool.gradebook.activity import ReportActivity
1036+ >>> root.templates['1'] = temp_ws = ReportWorksheet('1')
1037+ >>> temp_ws['1'] = temp_act = ReportActivity('1', None, PassFail)
1038+ >>> root.deployed['1'] = dep_ws = ReportWorksheet('1')
1039+ >>> dep_ws['1'] = dep_act = ReportActivity('1', None, AmericanLetterScoreSystem)
1040+
1041+ >>> from schooltool.gradebook.interfaces import IActivities
1042+ >>> activities = IActivities(section)
1043+
1044+ >>> from schooltool.requirement.scoresystem import ExtendedAmericanLetterScoreSystem
1045+ >>> from schooltool.gradebook.activity import Worksheet, Activity
1046+ >>> activities['1'] = worksheet = Worksheet('1')
1047+ >>> worksheet['1'] = activity = Activity('1', None, ExtendedAmericanLetterScoreSystem)
1048+
1049+ >>> from schooltool.requirement.interfaces import IHaveEvaluations
1050+ >>> from schooltool.requirement.interfaces import IEvaluations
1051+ >>> from zope.interface import alsoProvides
1052+ >>> alsoProvides(student, IHaveEvaluations)
1053+ >>> evaluations = IEvaluations(student)
1054+
1055+ >>> from schooltool.requirement.evaluation import Evaluation
1056+ >>> ev = Evaluation(activity, ExtendedAmericanLetterScoreSystem, 'A+', None)
1057+ >>> evaluations.addEvaluation(ev)
1058+
1059+ Finally, we'll run the evolve script, testing the effected values before and
1060+ after:
1061+
1062+ >>> from schooltool.requirement.interfaces import IScoreSystemsProxy
1063+ >>> proxy = IScoreSystemsProxy(app)
1064+ >>> proxy.getScoreSystems()
1065+ []
1066+
1067+ >>> temp_act.scoresystem
1068+ <GlobalDiscreteValuesScoreSystem u'Pass/Fail'>
1069+ >>> dep_act.scoresystem
1070+ <GlobalDiscreteValuesScoreSystem u'Letter Grade'>
1071+ >>> activity.scoresystem
1072+ <GlobalDiscreteValuesScoreSystem u'Extended Letter Grade'>
1073+ >>> ev.scoreSystem
1074+ <GlobalDiscreteValuesScoreSystem u'Extended Letter Grade'>
1075+
1076+ >>> evolve(context)
1077+
1078+ >>> proxy.getScoreSystems()
1079+ [(u'Extended Letter Grade', <CustomScoreSystem u'Extended Letter Grade'>),
1080+ (u'Letter Grade', <CustomScoreSystem u'Letter Grade'>),
1081+ (u'Pass/Fail', <CustomScoreSystem u'Pass/Fail'>)]
1082+
1083+ >>> temp_act.scoresystem
1084+ <CustomScoreSystem u'Pass/Fail'>
1085+ >>> dep_act.scoresystem
1086+ <CustomScoreSystem u'Letter Grade'>
1087+ >>> activity.scoresystem
1088+ <CustomScoreSystem u'Extended Letter Grade'>
1089+ >>> ev.scoreSystem
1090+ <CustomScoreSystem u'Extended Letter Grade'>
1091+ """
1092+
1093+
1094+def test_suite():
1095+ return unittest.TestSuite([
1096+ doctest.DocTestSuite(optionflags=doctest.ELLIPSIS
1097+ | doctest.NORMALIZE_WHITESPACE
1098+ | doctest.REPORT_NDIFF
1099+ | doctest.REPORT_ONLY_FIRST_FAILURE),
1100+ ])
1101+
1102+if __name__ == '__main__':
1103+ unittest.main(defaultTest='test_suite')
1104+
1105
1106=== modified file 'src/schooltool/gradebook/gradebook.py'
1107--- src/schooltool/gradebook/gradebook.py 2009-05-08 22:20:00 +0000
1108+++ src/schooltool/gradebook/gradebook.py 2009-06-01 21:32:36 +0000
1109@@ -23,7 +23,7 @@
1110 from zope.component import adapts, queryMultiAdapter
1111 from schooltool.app.interfaces import ISchoolToolApplication
1112 __docformat__ = 'reStructuredText'
1113-import persistent.dict
1114+from persistent.dict import PersistentDict
1115
1116 from decimal import Decimal
1117
1118@@ -53,6 +53,7 @@
1119 GRADEBOOK_SORTING_KEY = 'schooltool.gradebook.sorting'
1120 CURRENT_WORKSHEET_KEY = 'schooltool.gradebook.currentworksheet'
1121 DUE_DATE_FILTER_KEY = 'schooltool.gradebook.duedatefilter'
1122+COLUMN_PREFERENCES_KEY = 'schooltool.gradebook.columnpreferences'
1123
1124
1125 class WorksheetGradebookTraverser(object):
1126@@ -235,7 +236,7 @@
1127 person = proxy.removeSecurityProxy(person)
1128 ann = annotation.interfaces.IAnnotations(person)
1129 if CURRENT_WORKSHEET_KEY not in ann:
1130- ann[CURRENT_WORKSHEET_KEY] = persistent.dict.PersistentDict()
1131+ ann[CURRENT_WORKSHEET_KEY] = PersistentDict()
1132 if self.worksheets:
1133 default = self.worksheets[0]
1134 else:
1135@@ -248,7 +249,7 @@
1136 worksheet = proxy.removeSecurityProxy(worksheet)
1137 ann = annotation.interfaces.IAnnotations(person)
1138 if CURRENT_WORKSHEET_KEY not in ann:
1139- ann[CURRENT_WORKSHEET_KEY] = persistent.dict.PersistentDict()
1140+ ann[CURRENT_WORKSHEET_KEY] = PersistentDict()
1141 section_id = hash(IKeyReference(self.section))
1142 ann[CURRENT_WORKSHEET_KEY][section_id] = worksheet
1143
1144@@ -264,6 +265,18 @@
1145 ann = annotation.interfaces.IAnnotations(person)
1146 ann[DUE_DATE_FILTER_KEY] = (flag, weeks)
1147
1148+ def getColumnPreferences(self, person):
1149+ person = proxy.removeSecurityProxy(person)
1150+ ann = annotation.interfaces.IAnnotations(person)
1151+ if COLUMN_PREFERENCES_KEY not in ann:
1152+ return PersistentDict()
1153+ return ann[COLUMN_PREFERENCES_KEY]
1154+
1155+ def setColumnPreferences(self, person, columnPreferences):
1156+ person = proxy.removeSecurityProxy(person)
1157+ ann = annotation.interfaces.IAnnotations(person)
1158+ ann[COLUMN_PREFERENCES_KEY] = PersistentDict(columnPreferences)
1159+
1160 def getCurrentActivities(self, person):
1161 worksheet = self.getCurrentWorksheet(person)
1162 return self.getWorksheetActivities(worksheet)
1163@@ -297,7 +310,7 @@
1164 person = proxy.removeSecurityProxy(person)
1165 ann = annotation.interfaces.IAnnotations(person)
1166 if GRADEBOOK_SORTING_KEY not in ann:
1167- ann[GRADEBOOK_SORTING_KEY] = persistent.dict.PersistentDict()
1168+ ann[GRADEBOOK_SORTING_KEY] = PersistentDict()
1169 section_id = hash(IKeyReference(self.section))
1170 return ann[GRADEBOOK_SORTING_KEY].get(section_id, ('student', False))
1171
1172@@ -305,7 +318,7 @@
1173 person = proxy.removeSecurityProxy(person)
1174 ann = annotation.interfaces.IAnnotations(person)
1175 if GRADEBOOK_SORTING_KEY not in ann:
1176- ann[GRADEBOOK_SORTING_KEY] = persistent.dict.PersistentDict()
1177+ ann[GRADEBOOK_SORTING_KEY] = PersistentDict()
1178 section_id = hash(IKeyReference(self.section))
1179 ann[GRADEBOOK_SORTING_KEY][section_id] = value
1180
1181
1182=== modified file 'src/schooltool/gradebook/interfaces.py'
1183--- src/schooltool/gradebook/interfaces.py 2009-05-08 01:30:55 +0000
1184+++ src/schooltool/gradebook/interfaces.py 2009-06-01 21:32:36 +0000
1185@@ -236,6 +236,12 @@
1186 def setDueDateFilter(person, flag, weeks):
1187 """Set the user's current due date filter setting."""
1188
1189+ def getColumnPreferences(person):
1190+ """Get the user's column preferences."""
1191+
1192+ def setColumnPreferences(columnPreferences):
1193+ """Set the user's column preferences."""
1194+
1195 def getCurrentActivities(person):
1196 """Get the activities for the user's currently active worksheet."""
1197
1198
1199=== modified file 'src/schooltool/requirement/README.txt'
1200--- src/schooltool/requirement/README.txt 2008-09-25 03:01:46 +0000
1201+++ src/schooltool/requirement/README.txt 2009-05-14 08:34:27 +0000
1202@@ -179,7 +179,8 @@
1203 >>> from decimal import Decimal
1204 >>> check = scoresystem.DiscreteValuesScoreSystem(
1205 ... u'Check', u'Check-mark score system',
1206- ... [('+', Decimal(1)), ('v', Decimal(0)), ('-', Decimal(-1))])
1207+ ... [('+', Decimal(1), Decimal(80)), ('v', Decimal(0), Decimal(60)),
1208+ ... ('-', Decimal(-1), Decimal(0))])
1209
1210 The first and second arguments of the constructor are the title and
1211 description. The third argument is a list that really represents a mapping
1212@@ -247,9 +248,9 @@
1213 >>> from schooltool.requirement import scoresystem
1214 >>> check = scoresystem.DiscreteValuesScoreSystem(
1215 ... u'Check', u'Check-mark score system',
1216- ... [('+', Decimal(1)),
1217- ... ('v', Decimal(0)),
1218- ... ('-', Decimal(-1))],
1219+ ... [('+', Decimal(1), Decimal(80)),
1220+ ... ('v', Decimal(0), Decimal(60)),
1221+ ... ('-', Decimal(-1), Decimal(0))],
1222 ... minPassingScore='v')
1223 >>> check
1224 <DiscreteValuesScoreSystem u'Check'>
1225@@ -279,7 +280,8 @@
1226 >>> from schooltool.requirement import scoresystem
1227 >>> check = scoresystem.DiscreteValuesScoreSystem(
1228 ... u'Check', u'Check-mark score system',
1229- ... [('+', Decimal(1)), ('v', Decimal(0)), ('-', Decimal(-1))],
1230+ ... [('+', Decimal(1), Decimal(80)), ('v', Decimal(0), Decimal(60)),
1231+ ... ('-', Decimal(-1)), Decimal(0)],
1232 ... bestScore='+', minPassingScore='v')
1233
1234 >>> check.getBestScore()
1235@@ -297,7 +299,8 @@
1236 >>> scoresystem.PassFail.title
1237 u'Pass/Fail'
1238 >>> scoresystem.PassFail.scores
1239- [(u'Pass', Decimal("1")), (u'Fail', Decimal("0"))]
1240+ [(u'Pass', Decimal("1"), Decimal("60")),
1241+ (u'Fail', Decimal("0"), Decimal("0"))]
1242 >>> scoresystem.PassFail.isValidScore('Pass')
1243 True
1244 >>> scoresystem.PassFail.isPassingScore('Pass')
1245@@ -324,8 +327,9 @@
1246 >>> scoresystem.AmericanLetterScoreSystem.title
1247 u'Letter Grade'
1248 >>> scoresystem.AmericanLetterScoreSystem.scores
1249- [('A', Decimal("4")), ('B', Decimal("3")), ('C', Decimal("2")),
1250- ('D', Decimal("1")), ('F', Decimal("0"))]
1251+ [('A', Decimal("4"), Decimal("90")), ('B', Decimal("3"), Decimal("80")),
1252+ ('C', Decimal("2"), Decimal("70")), ('D', Decimal("1"), Decimal("60")),
1253+ ('F', Decimal("0"), Decimal("0"))]
1254 >>> scoresystem.AmericanLetterScoreSystem.isValidScore('C')
1255 True
1256 >>> scoresystem.AmericanLetterScoreSystem.isValidScore('E')
1257@@ -353,7 +357,7 @@
1258 'ExtendedAmericanLetterScoreSystem'
1259 >>> scoresystem.ExtendedAmericanLetterScoreSystem.title
1260 u'Extended Letter Grade'
1261- >>> [s for s, v in scoresystem.ExtendedAmericanLetterScoreSystem.scores]
1262+ >>> [s for s, v, p in scoresystem.ExtendedAmericanLetterScoreSystem.scores]
1263 ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']
1264 >>> scoresystem.ExtendedAmericanLetterScoreSystem.isValidScore('B-')
1265 True
1266@@ -842,6 +846,126 @@
1267 >>> len(evals)
1268 2
1269
1270+
1271+The ScoresSystemsProxy
1272+~~~~~~~~~~~~~~~~~~~~~~
1273+
1274+Score systems are utilities that are pre-created in code (eventually we will
1275+migrate these to the application's site manager) and user created ones that
1276+we add to the site manager by way of the ScoresSystemsProxy class.
1277+
1278+First we'll set up the site and initialize the app object.
1279+
1280+ >>> from schooltool.testing import setup
1281+ >>> app = setup.setUpSchoolToolSite()
1282+
1283+The ScoresSystemsProxy class is itself an adapter of the app object.
1284+
1285+ >>> from schooltool.app.interfaces import ISchoolToolApplication
1286+ >>> from schooltool.requirement.interfaces import IScoreSystemsProxy
1287+ >>> from schooltool.requirement.scoresystem import ScoreSystemsProxy
1288+ >>> from zope.component import provideAdapter
1289+ >>> provideAdapter(ScoreSystemsProxy)
1290+ >>> ssProxy = IScoreSystemsProxy(app)
1291+ >>> ssProxy
1292+ <schooltool.requirement.scoresystem.ScoreSystemsProxy object at ...>
1293+
1294+At fist, there are no scoresystems registered with the app.
1295+
1296+ >>> ssProxy.getScoreSystems()
1297+ []
1298+
1299+We'll create a couple of custom score systems and add them to the proxy.
1300+
1301+ >>> from schooltool.requirement.scoresystem import CustomScoreSystem
1302+ >>> custom1 = CustomScoreSystem('Custom 1')
1303+ >>> ssProxy.addScoreSystem( custom1)
1304+ >>> custom2 = CustomScoreSystem('Custom 2')
1305+ >>> ssProxy.addScoreSystem(custom2)
1306+
1307+Now, when we ask the proxy for what score systems it has, it will return both
1308+of the newly added ones.
1309+
1310+ >>> ssProxy.getScoreSystems()
1311+ [(u'Custom 1', <CustomScoreSystem 'Custom 1'>),
1312+ (u'Custom 2', <CustomScoreSystem 'Custom 2'>)]
1313+
1314+We can get one of them by name.
1315+
1316+ >>> ssProxy.getScoreSystem('Custom 1')
1317+ <CustomScoreSystem 'Custom 1'>
1318+
1319+
1320+Score System Vocabularies
1321+-------------------------
1322+
1323+Score System vocabularies are used to provide a pulldown for fields
1324+requiring scoresystem input. Let's register the utilities.
1325+
1326+ >>> from zope.component import provideUtility
1327+ >>> from schooltool.requirement import scoresystem
1328+
1329+First, the discrete values score systems.
1330+
1331+ >>> zope.component.provideUtility(
1332+ ... scoresystem.PassFail, interfaces.IDiscreteValuesScoreSystem,
1333+ ... u'Pass/Fail')
1334+ >>> zope.component.provideUtility(
1335+ ... scoresystem.AmericanLetterScoreSystem, interfaces.IDiscreteValuesScoreSystem,
1336+ ... u'Letter Grade')
1337+ >>> zope.component.provideUtility(
1338+ ... scoresystem.ExtendedAmericanLetterScoreSystem, interfaces.IDiscreteValuesScoreSystem,
1339+ ... u'Extended Letter Grade')
1340+
1341+Secondly, the ranged values score systems.
1342+
1343+ >>> zope.component.provideUtility(
1344+ ... scoresystem.PercentScoreSystem, interfaces.IScoreSystem,
1345+ ... u'Percent')
1346+ >>> zope.component.provideUtility(
1347+ ... scoresystem.HundredPointsScoreSystem, interfaces.IScoreSystem,
1348+ ... u'100 Points')
1349+
1350+Finally, the vocabularies.
1351+
1352+ >>> from zope.schema.vocabulary import getVocabularyRegistry
1353+ >>> getVocabularyRegistry().register(
1354+ ... 'schooltool.requirement.scoresystems',
1355+ ... scoresystem.ScoreSystemsVocabulary)
1356+ >>> getVocabularyRegistry().register(
1357+ ... 'schooltool.requirement.discretescoresystems',
1358+ ... scoresystem.DiscreteScoreSystemsVocabulary)
1359+
1360+Now, when we access the vocabularies as the application views will, we can test
1361+that they deliver the desired list of utilities.
1362+
1363+First the discrete values score systems vocabulary, used when only discrete
1364+values score systems are valid, like when converting an average into a discrete
1365+grade. Note the presence of the custom score systems we just created.
1366+
1367+ >>> from zope.app.component.vocabulary import UtilityVocabulary
1368+ >>> vocab = UtilityVocabulary(None, interface=interfaces.IDiscreteValuesScoreSystem)
1369+ >>> for v in vocab: print v
1370+ <UtilityTerm Custom 1, instance of CustomScoreSystem>
1371+ <UtilityTerm Custom 2, instance of CustomScoreSystem>
1372+ <UtilityTerm Extended Letter Grade, instance of GlobalDiscreteValuesScoreSystem>
1373+ <UtilityTerm Letter Grade, instance of GlobalDiscreteValuesScoreSystem>
1374+ <UtilityTerm Pass/Fail, instance of GlobalDiscreteValuesScoreSystem>
1375+
1376+Secondly, we have the general score systems vocabulary, returning all score
1377+systems registered. This is used for report sheet activities.
1378+
1379+ >>> vocab = UtilityVocabulary(None, interface=interfaces.IScoreSystem)
1380+ >>> for v in vocab: print v
1381+ <UtilityTerm 100 Points, instance of GlobalRangedValuesScoreSystem>
1382+ <UtilityTerm Custom 1, instance of CustomScoreSystem>
1383+ <UtilityTerm Custom 2, instance of CustomScoreSystem>
1384+ <UtilityTerm Extended Letter Grade, instance of GlobalDiscreteValuesScoreSystem>
1385+ <UtilityTerm Letter Grade, instance of GlobalDiscreteValuesScoreSystem>
1386+ <UtilityTerm Pass/Fail, instance of GlobalDiscreteValuesScoreSystem>
1387+ <UtilityTerm Percent, instance of GlobalRangedValuesScoreSystem>
1388+
1389+
1390 Epilogue
1391 --------
1392
1393
1394=== modified file 'src/schooltool/requirement/browser/README.txt'
1395--- src/schooltool/requirement/browser/README.txt 2008-04-17 17:22:08 +0000
1396+++ src/schooltool/requirement/browser/README.txt 2009-06-05 00:11:04 +0000
1397@@ -64,3 +64,69 @@
1398 >>> browser.getControl('1').click()
1399 >>> 'Be kind to your fellow students.' not in browser.contents
1400 True
1401+
1402+
1403+Score System Management
1404+-----------------------
1405+
1406+Score Systems can be created by the user to supplement the ones that are
1407+already delivered with schooltool. To do so, as manager, the user goes
1408+to the 'Manage' tab and clicks on the 'Score Systems' link.
1409+
1410+ >>> from schooltool.app.browser.ftests import setup
1411+ >>> manager = setup.logIn('manager', 'schooltool')
1412+ >>> manager.getLink('Manage').click()
1413+ >>> manager.getLink('Score Systems').click()
1414+
1415+We see the score systems that come with schooltool.
1416+
1417+ >>> analyze.printQuery("id('content-body')/form//a", manager.contents)
1418+ <a href="http://localhost/scoresystems/view.html?name=extended-letter-grade">Extended Letter Grade</a>
1419+ <a href="http://localhost/scoresystems/view.html?name=letter-grade">Letter Grade</a>
1420+ <a href="http://localhost/scoresystems/view.html?name=passfail">Pass/Fail</a>
1421+
1422+To add a new score system, the user clicks 'Add Score System'.
1423+
1424+ >>> manager.getLink('Add Score System').click()
1425+ >>> base_url = manager.url + '?form-submitted'
1426+ >>> cancel_url = base_url + '&CANCEL'
1427+ >>> update_url = base_url + '&UPDATE_SUBMIT'
1428+ >>> save_url = base_url + '&SAVE'
1429+
1430+We'll send the form values necessary to add a score system called 'Good/Bad'.
1431+
1432+ >>> url = save_url + '&title=Good/Bad&displayed1=G&value1=1&percent1=60'
1433+ >>> url = url + '&displayed2=B&value2=0&percent2=0'
1434+ >>> manager.open(url)
1435+
1436+Now we see the two score systems in the list.
1437+
1438+ >>> analyze.printQuery("id('content-body')/form//a", manager.contents)
1439+ <a href="http://localhost/scoresystems/view.html?name=extended-letter-grade">Extended Letter Grade</a>
1440+ <a href="http://localhost/scoresystems/view.html?name=goodbad">Good/Bad</a>
1441+ <a href="http://localhost/scoresystems/view.html?name=letter-grade">Letter Grade</a>
1442+ <a href="http://localhost/scoresystems/view.html?name=passfail">Pass/Fail</a>
1443+
1444+Let's hide the 'Pass/Fail' one.
1445+
1446+ >>> hide_url = manager.url + '?form-submitted&hide_passfail'
1447+ >>> manager.open(hide_url)
1448+
1449+Now we won't see it in the list.
1450+
1451+ >>> analyze.printQuery("id('content-body')/form//a", manager.contents)
1452+ <a href="http://localhost/scoresystems/view.html?name=extended-letter-grade">Extended Letter Grade</a>
1453+ <a href="http://localhost/scoresystems/view.html?name=goodbad">Good/Bad</a>
1454+ <a href="http://localhost/scoresystems/view.html?name=letter-grade">Letter Grade</a>
1455+
1456+Let's click on 'Good/Bad' and test it's view.
1457+
1458+ >>> manager.getLink('Good/Bad').click()
1459+ >>> analyze.printQuery("id('content-body')/table//span", manager.contents)
1460+ <span>G</span>
1461+ <span>1</span>
1462+ <span>60</span>
1463+ <span>B</span>
1464+ <span>0</span>
1465+ <span>0</span>
1466+
1467
1468=== modified file 'src/schooltool/requirement/browser/configure.zcml'
1469--- src/schooltool/requirement/browser/configure.zcml 2008-03-20 20:43:52 +0000
1470+++ src/schooltool/requirement/browser/configure.zcml 2009-05-08 18:19:21 +0000
1471@@ -3,8 +3,54 @@
1472 xmlns:zope="http://namespaces.zope.org/zope"
1473 i18n_domain="schooltool">
1474
1475+ <!-- Manage Tab -->
1476+ <configure package="schooltool.skin">
1477+ <navigationViewlet
1478+ name="scoresystems"
1479+ for="*"
1480+ manager="schooltool.app.browser.interfaces.IManageMenuViewletManager"
1481+ template="templates/navigationViewlet.pt"
1482+ class="schooltool.skin.skin.NavigationViewlet"
1483+ permission="schooltool.edit"
1484+ link="scoresystems"
1485+ title="Score Systems"
1486+ order="220"
1487+ />
1488+ </configure>
1489+
1490+ <!-- Menu items for IScoreSystemsProxy -->
1491+ <menuItem
1492+ menu="schooltool_actions"
1493+ title="Add Score System"
1494+ for="schooltool.requirement.interfaces.IScoreSystemsProxy"
1495+ action="add.html"
1496+ permission="schooltool.edit"
1497+ />
1498+
1499+ <!-- Score System Views -->
1500+ <page
1501+ name="index.html"
1502+ for="schooltool.requirement.interfaces.IScoreSystemsProxy"
1503+ class=".scoresystem.ScoreSystemsView"
1504+ template="scoresystems_overview.pt"
1505+ permission="schooltool.edit"
1506+ />
1507+ <page
1508+ name="add.html"
1509+ for="schooltool.requirement.interfaces.IScoreSystemsProxy"
1510+ class=".scoresystem.ScoreSystemAddView"
1511+ template="scoresystem_add.pt"
1512+ permission="schooltool.edit"
1513+ />
1514+ <page
1515+ name="view.html"
1516+ for="schooltool.requirement.interfaces.IScoreSystemsProxy"
1517+ class=".scoresystem.ScoreSystemViewView"
1518+ template="scoresystem_view.pt"
1519+ permission="schooltool.edit"
1520+ />
1521+
1522 <!-- Score system widget registration -->
1523-
1524 <zope:view
1525 type="zope.publisher.interfaces.browser.IBrowserRequest"
1526 for="schooltool.requirement.scoresystem.IScoreSystemField"
1527@@ -12,7 +58,6 @@
1528 factory=".scoresystem.ScoreSystemWidget"
1529 permission="zope.Public"
1530 />
1531-
1532 <page
1533 name="index.html"
1534 for="schooltool.requirement.interfaces.IRequirement"
1535@@ -22,14 +67,12 @@
1536 menu="zmi_views"
1537 title="View"
1538 />
1539-
1540 <page
1541 name="treenode"
1542 for="schooltool.requirement.interfaces.IRequirement"
1543 template="treenode.pt"
1544 permission="schooltool.view"
1545 />
1546-
1547 <containerViews
1548 for="..interfaces.IRequirement"
1549 contents="zope.ManageContent"
1550
1551=== modified file 'src/schooltool/requirement/browser/ftesting.zcml'
1552--- src/schooltool/requirement/browser/ftesting.zcml 2008-10-22 15:50:26 +0000
1553+++ src/schooltool/requirement/browser/ftesting.zcml 2009-05-08 18:19:21 +0000
1554@@ -9,6 +9,7 @@
1555 <include package="schooltool.schoolyear" />
1556 <include package="schooltool.timetable" />
1557 <include package="schooltool.requirement" />
1558+ <include package="schooltool.gradebook" />
1559
1560 <configure
1561 package="schooltool.skin"
1562
1563=== modified file 'src/schooltool/requirement/browser/scoresystem.py'
1564--- src/schooltool/requirement/browser/scoresystem.py 2008-04-16 18:57:28 +0000
1565+++ src/schooltool/requirement/browser/scoresystem.py 2009-05-14 08:34:27 +0000
1566@@ -21,14 +21,216 @@
1567 $Id$
1568 """
1569 __docformat__ = 'reStructuredText'
1570+
1571+from decimal import Decimal
1572+
1573 import zope.interface
1574 import zope.schema
1575 from zope.app import form
1576 from zope.app.pagetemplate import ViewPageTemplateFile
1577+from zope.publisher.browser import BrowserView
1578+from zope.security.proxy import removeSecurityProxy
1579+from zope.traversing.browser.absoluteurl import absoluteURL
1580
1581 from schooltool.common import SchoolToolMessage as _
1582 from schooltool.requirement import interfaces, scoresystem
1583
1584+
1585+MISSING_TITLE = _('The Title field must not be empty.')
1586+VALUE_NOT_NUMERIC = _('Value field must contain a valid number.')
1587+PERCENT_NOT_NUMERIC = _('Percent field must contain a valid number.')
1588+NO_NEGATIVE_VALUES = _('All values must be non-negative.')
1589+NO_NEGATIVE_PERCENTS = _('All percentages must be non-negative.')
1590+NO_PERCENTS_OVER_100 = _('Percentages cannot be greater than 100.')
1591+MUST_HAVE_AT_LEAST_2_SCORES = _('A score system must have at least two scores.')
1592+VALUES_MUST_DESCEND = _('Score values must go in descending order.')
1593+PERCENTS_MUST_DESCEND = _('Score percentages must go in descending order.')
1594+LAST_PERCENT_NOT_ZERO = _('The last percentage must be zero.')
1595+
1596+
1597+def escName(name):
1598+ """converts title-based scoresystem name to querystring format"""
1599+ chars = [c for c in name.lower() if c.isalnum() or c == ' ']
1600+ return u''.join(chars).replace(' ', '-')
1601+
1602+
1603+class ScoreSystemsView(BrowserView):
1604+ """A view for maintaining user-created scoresystem utilities"""
1605+
1606+ def update(self):
1607+ if 'form-submitted' in self.request:
1608+ for name, ss in self.context.getScoreSystems():
1609+ ss = removeSecurityProxy(ss)
1610+ if 'hide_' + escName(name) in self.request:
1611+ ss.hidden = True
1612+
1613+ def scoresystems(self):
1614+ url = absoluteURL(self.context, self.request) + '/view.html'
1615+ results = []
1616+ for name, ss in self.context.getScoreSystems():
1617+ ss = removeSecurityProxy(ss)
1618+ result = {
1619+ 'title': ss.title,
1620+ 'url': '%s?name=%s' % (url, escName(name)),
1621+ 'hide_name': 'hide_' + escName(name),
1622+ }
1623+ results.append(result)
1624+ return results
1625+
1626+
1627+class ScoreSystemAddView(BrowserView):
1628+ """A view for adding a user-created scoresystem utility"""
1629+
1630+ def update(self):
1631+ self.message = ''
1632+
1633+ if 'form-submitted' in self.request:
1634+ if 'CANCEL' in self.request:
1635+ self.request.response.redirect(self.nextURL())
1636+
1637+ if not self.validateForm():
1638+ return
1639+ if 'SAVE' in self.request:
1640+ if not self.validateScores():
1641+ return
1642+ target = scoresystem.CustomScoreSystem()
1643+ self.updateScoreSystem(target)
1644+ self.context.addScoreSystem(target)
1645+ self.request.response.redirect(self.nextURL())
1646+
1647+ def nextURL(self):
1648+ return absoluteURL(self.context, self.request)
1649+
1650+ def scores(self):
1651+ rownum = 1
1652+ results = []
1653+ for displayed, value, percent in self.getRequestScores():
1654+ results.append(self.buildScoreRow(rownum, displayed, value,
1655+ percent))
1656+ rownum += 1
1657+ results.append(self.buildScoreRow(rownum, '', '', ''))
1658+ return results
1659+
1660+ def buildScoreRow(self, rownum, displayed, value, percent):
1661+ return {
1662+ 'displayed_name': 'displayed' + unicode(rownum),
1663+ 'displayed_value': displayed,
1664+ 'value_name': 'value' + unicode(rownum),
1665+ 'value_value': value,
1666+ 'percent_name': 'percent' + unicode(rownum),
1667+ 'percent_value': percent,
1668+ }
1669+
1670+ def getRequestScores(self):
1671+ rownum = 0
1672+ results = []
1673+ while True:
1674+ rownum += 1
1675+ displayed_name = 'displayed' + unicode(rownum)
1676+ value_name = 'value' + unicode(rownum)
1677+ percent_name = 'percent' + unicode(rownum)
1678+ if displayed_name not in self.request:
1679+ break
1680+ if not len(self.request[displayed_name]):
1681+ continue
1682+ result = (self.request[displayed_name],
1683+ self.request[value_name],
1684+ self.request[percent_name])
1685+ results.append(result)
1686+ return results
1687+
1688+ def validateForm(self):
1689+ title = self.request['title']
1690+ if not len(title):
1691+ return self.setMessage(MISSING_TITLE)
1692+
1693+ scores = []
1694+ for displayed, value, percent in self.getRequestScores():
1695+ try:
1696+ decimal_value = Decimal(value)
1697+ except:
1698+ return self.setMessage(VALUE_NOT_NUMERIC)
1699+ if decimal_value < 0:
1700+ return self.setMessage(NO_NEGATIVE_VALUES)
1701+ try:
1702+ decimal_percent = Decimal(percent)
1703+ except:
1704+ return self.setMessage(PERCENT_NOT_NUMERIC)
1705+ if decimal_percent < 0:
1706+ return self.setMessage(NO_NEGATIVE_PERCENTS)
1707+ if decimal_percent > 100:
1708+ return self.setMessage(NO_PERCENTS_OVER_100)
1709+ scores.append([displayed, decimal_value, decimal_percent])
1710+
1711+ self.validTitle = title
1712+ self.validScores = scores
1713+ return True
1714+
1715+ def validateScores(self):
1716+ if len(self.validScores) < 2:
1717+ return self.setMessage(MUST_HAVE_AT_LEAST_2_SCORES)
1718+
1719+ last_value, last_percent = None, None
1720+ for displayed, value, percent in self.validScores:
1721+ if last_value is not None:
1722+ if value >= last_value:
1723+ return self.setMessage(VALUES_MUST_DESCEND)
1724+ if last_percent is not None:
1725+ if percent >= last_percent:
1726+ return self.setMessage(PERCENTS_MUST_DESCEND)
1727+ last_value = value
1728+ last_percent = percent
1729+
1730+ if last_percent <> 0:
1731+ return self.setMessage(LAST_PERCENT_NOT_ZERO)
1732+
1733+ return True
1734+
1735+ def setMessage(self, message):
1736+ self.message = message
1737+ return False
1738+
1739+ def updateScoreSystem(self, target):
1740+ target.title = self.validTitle
1741+ target.scores = self.validScores
1742+ target._bestScore = target.scores[0][1]
1743+
1744+ @property
1745+ def title_value(self):
1746+ if 'form-submitted' in self.request:
1747+ return self.request['title']
1748+ else:
1749+ return ''
1750+
1751+
1752+class ScoreSystemViewView(BrowserView):
1753+ """A view for viewing a user-created scoresystem utility"""
1754+
1755+ def scores(self):
1756+ target = self.getScoreSystem()
1757+ return [self.buildScoreRow(displayed, value, percent)
1758+ for displayed, value, percent in target.scores]
1759+
1760+ def buildScoreRow(self, displayed, value, percent):
1761+ return {
1762+ 'displayed_value': displayed,
1763+ 'value_value': value,
1764+ 'percent_value': percent,
1765+ }
1766+
1767+ @property
1768+ def title_value(self):
1769+ target = self.getScoreSystem()
1770+ return target.title
1771+
1772+ def getScoreSystem(self):
1773+ name = self.request['QUERY_STRING'].split('=')[1]
1774+ for n, ss in self.context.getScoreSystems():
1775+ if escName(n) == name:
1776+ return removeSecurityProxy(ss)
1777+ raise KeyError
1778+
1779+
1780 class IWidgetData(interfaces.IRangedValuesScoreSystem):
1781 """A schema used to generate the score system widget."""
1782
1783
1784=== added file 'src/schooltool/requirement/browser/scoresystem_add.pt'
1785--- src/schooltool/requirement/browser/scoresystem_add.pt 1970-01-01 00:00:00 +0000
1786+++ src/schooltool/requirement/browser/scoresystem_add.pt 2009-05-08 18:19:21 +0000
1787@@ -0,0 +1,65 @@
1788+<tal:define define="dummy view/update"/>
1789+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1790+<head>
1791+ <title metal:fill-slot="title" i18n:translate="">Add Score System</title>
1792+</head>
1793+<body>
1794+
1795+<h1 metal:fill-slot="content-header"
1796+ i18n:translate="">Add Score System</h1>
1797+
1798+<metal:block metal:fill-slot="body">
1799+
1800+ <div class="message" style="color:red; padding:1em"
1801+ tal:condition="view/message"
1802+ tal:content="view/message">
1803+ Message
1804+ </div>
1805+
1806+
1807+ <form method="post"
1808+ tal:attributes="action string:${context/@@absolute_url}/add.html">
1809+ <input type="hidden" name="form-submitted" value="" />
1810+
1811+ <label for="title" class="bold padded" i18n:translate="">Title</label>
1812+ <input type="text" name="title" id="title"
1813+ tal:attributes="value view/title_value" />
1814+ <span>&nbsp&nbsp</span>
1815+ <input type="submit" class="button-ok" name="SAVE" value="Submit" />
1816+ <div style="height: 31px;"></div>
1817+
1818+ <table class="schooltool_gradebook">
1819+ <tr>
1820+ <th class="cell header fully_padded">Score Displayed</th>
1821+ <th class="cell header fully_padded">Score Value</th>
1822+ <th class="cell header fully_padded">Low Persntage</th>
1823+ </tr>
1824+
1825+ <tal:block repeat="score view/scores">
1826+ <tr class="bordered">
1827+ <td class="cell fully_padded">
1828+ <input type="text"
1829+ tal:attributes="name score/displayed_name; value score/displayed_value" />
1830+ </td>
1831+ <td class="cell fully_padded">
1832+ <input type="text"
1833+ tal:attributes="name score/value_name; value score/value_value" />
1834+ </td>
1835+ <td class="cell fully_padded">
1836+ <input type="text"
1837+ tal:attributes="name score/percent_name; value score/percent_value" />
1838+ </td>
1839+ </tr>
1840+ </tal:block>
1841+
1842+ </table>
1843+ <div style="height: 11px;"></div>
1844+
1845+ <div class="controls">
1846+ <input type="submit" class="button-ok" name="UPDATE_SUBMIT" value="Update" />
1847+ <input type="submit" class="button-cancel" name="CANCEL" value="Cancel" />
1848+ </div>
1849+ </form>
1850+</metal:block>
1851+</body>
1852+</html>
1853
1854=== added file 'src/schooltool/requirement/browser/scoresystem_view.pt'
1855--- src/schooltool/requirement/browser/scoresystem_view.pt 1970-01-01 00:00:00 +0000
1856+++ src/schooltool/requirement/browser/scoresystem_view.pt 2009-06-05 00:11:04 +0000
1857@@ -0,0 +1,41 @@
1858+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1859+<head>
1860+ <title metal:fill-slot="title" i18n:translate="">View Score System</title>
1861+</head>
1862+<body>
1863+
1864+<h1 metal:fill-slot="content-header"
1865+ i18n:translate="">View Score System</h1>
1866+
1867+<metal:block metal:fill-slot="body">
1868+
1869+ <b>Title:</b>
1870+ <span tal:content="view/title_value" />
1871+ <div style="height: 31px;"></div>
1872+
1873+ <table class="schooltool_gradebook">
1874+ <tr>
1875+ <th class="cell header fully_padded">Score Displayed</th>
1876+ <th class="cell header fully_padded">Score Value</th>
1877+ <th class="cell header fully_padded">Low Percentage</th>
1878+ </tr>
1879+
1880+ <tal:block repeat="score view/scores">
1881+ <tr class="bordered">
1882+ <td class="cell fully_padded">
1883+ <span tal:content="score/displayed_value" />
1884+ </td>
1885+ <td class="cell fully_padded">
1886+ <span tal:content="score/value_value" />
1887+ </td>
1888+ <td class="cell fully_padded">
1889+ <span tal:content="score/percent_value" />
1890+ </td>
1891+ </tr>
1892+ </tal:block>
1893+
1894+ </table>
1895+
1896+</metal:block>
1897+</body>
1898+</html>
1899
1900=== added file 'src/schooltool/requirement/browser/scoresystems_overview.pt'
1901--- src/schooltool/requirement/browser/scoresystems_overview.pt 1970-01-01 00:00:00 +0000
1902+++ src/schooltool/requirement/browser/scoresystems_overview.pt 2009-05-08 18:19:21 +0000
1903@@ -0,0 +1,44 @@
1904+<tal:define define="dummy view/update"/>
1905+<html metal:use-macro="view/@@standard_macros/page" i18n:domain="schooltool">
1906+<head>
1907+ <title metal:fill-slot="title" i18n:translate="">Score Systems</title>
1908+</head>
1909+<body>
1910+
1911+<h1 metal:fill-slot="content-header"
1912+ i18n:translate="">Score Systems</h1>
1913+
1914+<metal:block metal:fill-slot="body"
1915+ tal:define="scoresystems view/scoresystems">
1916+
1917+ <form method="post"
1918+ tal:attributes="action string:${context/@@absolute_url}/index.html">
1919+ <input type="hidden" name="form-submitted" value="" />
1920+
1921+ <table class="schooltool_gradebook">
1922+ <tr>
1923+ <th class="cell header fully_padded">Hide?</th>
1924+ <th class="cell header fully_padded">Score System</th>
1925+ </tr>
1926+
1927+ <tr class="bordered"
1928+ tal:repeat="scoresystem scoresystems">
1929+ <td class="cell fully_padded">
1930+ <input type="checkbox"
1931+ tal:attributes="name scoresystem/hide_name;" />
1932+ </td>
1933+ <td class="cell fully_padded">
1934+ <a tal:content="scoresystem/title"
1935+ tal:attributes="href scoresystem/url">Score System</a>
1936+ </td>
1937+ </tr>
1938+ </table>
1939+
1940+ <div class="controls">
1941+ <input type="submit" class="button-ok" name="UPDATE_SUBMIT" value="Update" />
1942+ </div>
1943+
1944+ </form>
1945+</metal:block>
1946+</body>
1947+</html>
1948
1949=== modified file 'src/schooltool/requirement/browser/tests.py'
1950--- src/schooltool/requirement/browser/tests.py 2008-03-19 04:22:15 +0000
1951+++ src/schooltool/requirement/browser/tests.py 2009-05-08 18:19:21 +0000
1952@@ -243,12 +243,14 @@
1953 return unittest.TestSuite((
1954 doctest.DocTestSuite(setUp=setUp, tearDown=tearDown,
1955 optionflags=doctest.ELLIPSIS|
1956- doctest.REPORT_NDIFF),
1957+ doctest.REPORT_NDIFF|
1958+ doctest.REPORT_ONLY_FIRST_FAILURE),
1959 doctest.DocFileSuite('scoresystem.txt',
1960 setUp=setUp, tearDown=tearDown,
1961 globs={'pprint': doctestunit.pprint},
1962 optionflags=doctest.NORMALIZE_WHITESPACE|
1963- doctest.ELLIPSIS),
1964+ doctest.ELLIPSIS|
1965+ doctest.REPORT_ONLY_FIRST_FAILURE),
1966 ))
1967
1968
1969
1970=== modified file 'src/schooltool/requirement/configure.zcml'
1971--- src/schooltool/requirement/configure.zcml 2008-09-14 22:04:05 +0000
1972+++ src/schooltool/requirement/configure.zcml 2009-06-05 00:11:04 +0000
1973@@ -10,29 +10,26 @@
1974 factory=".requirement.getRequirement"
1975 trusted="true"
1976 />
1977-
1978 <adapter
1979 for=".interfaces.IHaveEvaluations"
1980 provides=".interfaces.IEvaluations"
1981 factory=".evaluation.getEvaluations"
1982 trusted="true"
1983 />
1984-
1985 <view
1986 name="requirement" type="*"
1987 provides="zope.traversing.interfaces.ITraversable"
1988- for="schooltool.requirement.interfaces.IHaveRequirement"
1989+ for=".interfaces.IHaveRequirement"
1990 factory=".requirement.requirementNamespace"
1991 />
1992 <adapter
1993 name="requirement"
1994 provides="zope.traversing.interfaces.ITraversable"
1995- for="schooltool.requirement.interfaces.IHaveRequirement"
1996+ for=".interfaces.IHaveRequirement"
1997 factory=".requirement.requirementNamespace"
1998 />
1999
2000 <!-- Requirement Content -->
2001-
2002 <class class=".requirement.Requirement">
2003 <allow interface="zope.app.container.interfaces.ISimpleReadContainer" />
2004 <require
2005@@ -50,7 +47,6 @@
2006 </class>
2007
2008 <!-- Scoresystem Content -->
2009-
2010 <class class=".scoresystem.DiscreteValuesScoreSystem">
2011 <require
2012 permission="zope.View"
2013@@ -61,7 +57,6 @@
2014 set_schema=".interfaces.IScoreSystem"
2015 />
2016 </class>
2017-
2018 <class class=".scoresystem.GlobalDiscreteValuesScoreSystem">
2019 <require
2020 permission="zope.View"
2021@@ -72,7 +67,6 @@
2022 set_schema=".interfaces.IScoreSystem"
2023 />
2024 </class>
2025-
2026 <class class=".scoresystem.RangedValuesScoreSystem">
2027 <require
2028 permission="zope.View"
2029@@ -83,7 +77,6 @@
2030 set_schema=".interfaces.IScoreSystem"
2031 />
2032 </class>
2033-
2034 <class class=".scoresystem.GlobalRangedValuesScoreSystem">
2035 <require
2036 permission="zope.View"
2037@@ -94,55 +87,46 @@
2038 set_schema=".interfaces.IScoreSystem"
2039 />
2040 </class>
2041+ <class class=".scoresystem.CustomScoreSystem">
2042+ <require
2043+ permission="zope.View"
2044+ interface=".interfaces.ICustomScoreSystem"
2045+ />
2046+ <require
2047+ permission="schooltool.edit"
2048+ set_schema=".interfaces.ICustomScoreSystem"
2049+ />
2050+ </class>
2051
2052 <!-- Score System registrations -->
2053-
2054 <utility
2055 provides="zope.schema.interfaces.IVocabularyFactory"
2056- component="schooltool.requirement.scoresystem.ScoreSystemsVocabulary"
2057+ component=".scoresystem.ScoreSystemsVocabulary"
2058 name="schooltool.requirement.scoresystems"
2059 />
2060-
2061- <utility
2062- provides="schooltool.requirement.interfaces.IScoreSystem"
2063- component="schooltool.requirement.scoresystem.PassFail"
2064- name="Pass/Fail"
2065- />
2066-
2067- <utility
2068- provides="schooltool.requirement.interfaces.IScoreSystem"
2069- component="schooltool.requirement.scoresystem.AmericanLetterScoreSystem"
2070- name="Letter Grade"
2071- />
2072-
2073- <utility
2074- provides="schooltool.requirement.interfaces.IScoreSystem"
2075- component="
2076- schooltool.requirement.scoresystem.ExtendedAmericanLetterScoreSystem"
2077- name="Extended Letter Grade"
2078- />
2079-
2080- <utility
2081- provides="schooltool.requirement.interfaces.IScoreSystem"
2082- component="schooltool.requirement.scoresystem.PercentScoreSystem"
2083+ <utility
2084+ provides="zope.schema.interfaces.IVocabularyFactory"
2085+ component=".scoresystem.DiscreteScoreSystemsVocabulary"
2086+ name="schooltool.requirement.discretescoresystems"
2087+ />
2088+ <utility
2089+ provides="schooltool.requirement.interfaces.IScoreSystem"
2090+ component=".scoresystem.PercentScoreSystem"
2091 name="Percent"
2092 />
2093-
2094 <utility
2095 provides="schooltool.requirement.interfaces.IScoreSystem"
2096- component="schooltool.requirement.scoresystem.HundredPointsScoreSystem"
2097+ component=".scoresystem.HundredPointsScoreSystem"
2098 name="100 Points"
2099 />
2100
2101 <!-- Evaluations Content -->
2102-
2103 <class class=".evaluation.Evaluations">
2104 <require
2105 permission="schooltool.edit"
2106 interface=".interfaces.IEvaluations"
2107 />
2108 </class>
2109-
2110 <class class=".evaluation.Evaluation">
2111 <require
2112 permission="schooltool.view"
2113@@ -155,24 +139,42 @@
2114 </class>
2115
2116 <!-- These declarations should go somewhere else eventually -->
2117-
2118 <class class="schooltool.app.app.SchoolToolApplication">
2119 <implements
2120- interface="schooltool.requirement.interfaces.IHaveRequirement" />
2121+ interface=".interfaces.IHaveRequirement" />
2122 </class>
2123-
2124 <class class="schooltool.course.course.Course">
2125 <implements interface=".interfaces.IHaveRequirement" />
2126 </class>
2127-
2128 <class class="schooltool.course.section.Section">
2129 <implements interface=".interfaces.IHaveRequirement" />
2130 </class>
2131-
2132 <class class="schooltool.person.person.Person">
2133 <implements interface=".interfaces.IHaveEvaluations" />
2134 </class>
2135
2136+ <!-- ScoreSystemsProxy adapter -->
2137+ <class class=".scoresystem.ScoreSystemsProxy">
2138+ <require
2139+ permission="schooltool.edit"
2140+ interface=".interfaces.IScoreSystemsProxy" />
2141+ </class>
2142+ <adapter
2143+ for="schooltool.app.app.SchoolToolApplication"
2144+ provides=".interfaces.IScoreSystemsProxy"
2145+ factory=".scoresystem.ScoreSystemsProxy"
2146+ trusted="true"
2147+ />
2148+
2149+ <!-- Pluggable traverser plugins for HTTP paths -->
2150+ <adapterTraverserPlugin
2151+ for="schooltool.app.interfaces.ISchoolToolApplication"
2152+ layer="zope.publisher.interfaces.http.IHTTPRequest"
2153+ name="scoresystems"
2154+ adapter=".interfaces.IScoreSystemsProxy"
2155+ permission="schooltool.edit"
2156+ />
2157+
2158 <include package=".browser" />
2159
2160 </configure>
2161
2162=== modified file 'src/schooltool/requirement/interfaces.py'
2163--- src/schooltool/requirement/interfaces.py 2009-04-29 00:26:02 +0000
2164+++ src/schooltool/requirement/interfaces.py 2009-05-08 18:19:21 +0000
2165@@ -116,13 +116,22 @@
2166 class IDiscreteValuesScoreSystem(IValuesScoreSystem):
2167 """A score system that consists of discrete values."""
2168
2169+ hidden = zope.schema.Bool(
2170+ title=u"Hidden Score System",
2171+ required=False
2172+ )
2173+
2174 scores = zope.schema.List(
2175 title=u'Scores',
2176- description=u'A list of 2-tuples of the form (score, numerical value).',
2177+ description=u'A list of 3-tuples of the form (score, value, percent).',
2178 value_type=zope.schema.Tuple(),
2179 required=True)
2180
2181
2182+class ICustomScoreSystem(IDiscreteValuesScoreSystem):
2183+ """A user-created score system that consists of discrete values."""
2184+
2185+
2186 class IRangedValuesScoreSystem(IValuesScoreSystem):
2187 """A score system that allows for a randge of values."""
2188
2189@@ -232,3 +241,16 @@
2190 name that the original ``IEvaluations`` object had.
2191 """
2192
2193+
2194+class IScoreSystemsProxy(zope.interface.Interface):
2195+ """The Proxy class for adding/editing score systems"""
2196+
2197+ def getScoreSystems():
2198+ """Return list of tuples (name, scoresystem)"""
2199+
2200+ def addScoreSystem(scoresystem):
2201+ """Add scoresystem to app utilitiles"""
2202+
2203+ def getScoreSystem(name):
2204+ """Get scoresystem from app utilitiles by the given name"""
2205+
2206
2207=== modified file 'src/schooltool/requirement/scoresystem.py'
2208--- src/schooltool/requirement/scoresystem.py 2008-09-25 03:01:46 +0000
2209+++ src/schooltool/requirement/scoresystem.py 2009-05-14 08:34:27 +0000
2210@@ -22,20 +22,34 @@
2211 """
2212 __docformat__ = 'restructuredtext'
2213
2214+from decimal import Decimal
2215+
2216+from persistent import Persistent
2217+
2218+from zope.app.component.vocabulary import UtilityVocabulary
2219+from zope.component import adapts, queryMultiAdapter
2220+from zope.interface import implements
2221 import zope.interface
2222 import zope.schema
2223 import zope.security.checker
2224-from zope.app.component.vocabulary import UtilityVocabulary
2225-
2226-from decimal import Decimal
2227-
2228+from zope.security.proxy import removeSecurityProxy
2229+
2230+from schooltool.app.interfaces import ISchoolToolApplication
2231 from schooltool.requirement import interfaces
2232+from schooltool.requirement.interfaces import IDiscreteValuesScoreSystem
2233+from schooltool.requirement.interfaces import IScoreSystemsProxy
2234
2235
2236 def ScoreSystemsVocabulary(context):
2237 return UtilityVocabulary(context,
2238 interface=interfaces.IScoreSystem)
2239
2240+
2241+def DiscreteScoreSystemsVocabulary(context):
2242+ return UtilityVocabulary(context,
2243+ interface=interfaces.IDiscreteValuesScoreSystem)
2244+
2245+
2246 class UNSCORED(object):
2247 """This object behaves like a string.
2248
2249@@ -49,9 +63,11 @@
2250 def __repr__(self):
2251 return 'UNSCORED'
2252
2253+
2254 zope.security.checker.BasicTypes[UNSCORED] = zope.security.checker.NoProxy
2255 UNSCORED = UNSCORED()
2256
2257+
2258 class AbstractScoreSystem(object):
2259 zope.interface.implements(interfaces.IScoreSystem)
2260
2261@@ -88,6 +104,7 @@
2262 def __reduce__(self):
2263 return 'CommentScoreSystem'
2264
2265+
2266 # Singelton
2267 CommentScoreSystem = CommentScoreSystem(
2268 u'Comments', u'Scores are commentary text.')
2269@@ -126,6 +143,7 @@
2270 scores = None
2271 _minPassingScore = None
2272 _bestScore = None
2273+ hidden = False
2274
2275 def __init__(self, title=None, description=None,
2276 scores=None, bestScore=None, minPassingScore=None):
2277@@ -141,12 +159,12 @@
2278 return None
2279 if self._minPassingScore is None:
2280 return None
2281- scores = dict(self.scores)
2282+ scores = self.scoresDict()
2283 return scores[score] >= scores[self._minPassingScore]
2284
2285 def isValidScore(self, score):
2286 """See interfaces.IScoreSystem"""
2287- scores = dict(self.scores).keys()
2288+ scores = self.scoresDict().keys()
2289 return score in scores + [UNSCORED]
2290
2291 def getBestScore(self):
2292@@ -167,7 +185,7 @@
2293 """See interfaces.IScoreSystem"""
2294 if score is UNSCORED:
2295 return None
2296- scores = dict(self.scores)
2297+ scores = self.scoresDict()
2298 return scores[score]
2299
2300 def getFractionalValue(self, score):
2301@@ -179,6 +197,10 @@
2302 value = self.getNumericalValue(score) - minimum
2303 return value / (maximum - minimum)
2304
2305+ def scoresDict(self):
2306+ scores = [(score, value) for score, value, percent in self.scores]
2307+ return dict(scores)
2308+
2309 class GlobalDiscreteValuesScoreSystem(DiscreteValuesScoreSystem):
2310
2311 def __init__(self, name, *args, **kwargs):
2312@@ -188,25 +210,41 @@
2313 def __reduce__(self):
2314 return self.__name__
2315
2316+
2317 PassFail = GlobalDiscreteValuesScoreSystem(
2318 'PassFail',
2319 u'Pass/Fail', u'Pass or Fail score system.',
2320- [(u'Pass', Decimal(1)), (u'Fail', Decimal(0))], u'Pass', u'Pass')
2321+ [(u'Pass', Decimal(1), Decimal(60)),
2322+ (u'Fail', Decimal(0), Decimal(0))],
2323+ u'Pass', u'Pass')
2324
2325 AmericanLetterScoreSystem = GlobalDiscreteValuesScoreSystem(
2326 'AmericanLetterScoreSystem',
2327 u'Letter Grade', u'American Letter Grade',
2328- [('A', Decimal(4)), ('B', Decimal(3)), ('C', Decimal(2)),
2329- ('D', Decimal(1)), ('F', Decimal(0))], 'A', 'D')
2330+ [('A', Decimal(4), Decimal(90)),
2331+ ('B', Decimal(3), Decimal(80)),
2332+ ('C', Decimal(2), Decimal(70)),
2333+ ('D', Decimal(1), Decimal(60)),
2334+ ('F', Decimal(0), Decimal(0))],
2335+ 'A', 'D')
2336
2337 ExtendedAmericanLetterScoreSystem = GlobalDiscreteValuesScoreSystem(
2338 'ExtendedAmericanLetterScoreSystem',
2339 u'Extended Letter Grade', u'American Extended Letter Grade',
2340- [('A+', Decimal('4.0')), ('A', Decimal('4.0')), ('A-', Decimal('3.7')),
2341- ('B+', Decimal('3.3')), ('B', Decimal('3.0')), ('B-', Decimal('2.7')),
2342- ('C+', Decimal('2.3')), ('C', Decimal('2.0')), ('C-', Decimal('1.7')),
2343- ('D+', Decimal('1.3')), ('D', Decimal('1.0')), ('D-', Decimal('0.7')),
2344- ('F', Decimal('0.0'))], 'A+', 'D-')
2345+ [('A+', Decimal('4.0'), Decimal(98)),
2346+ ('A', Decimal('4.0'), Decimal(93)),
2347+ ('A-', Decimal('3.7'), Decimal(90)),
2348+ ('B+', Decimal('3.3'), Decimal(88)),
2349+ ('B', Decimal('3.0'), Decimal(83)),
2350+ ('B-', Decimal('2.7'), Decimal(80)),
2351+ ('C+', Decimal('2.3'), Decimal(78)),
2352+ ('C', Decimal('2.0'), Decimal(73)),
2353+ ('C-', Decimal('1.7'), Decimal(70)),
2354+ ('D+', Decimal('1.3'), Decimal(68)),
2355+ ('D', Decimal('1.0'), Decimal(63)),
2356+ ('D-', Decimal('0.7'), Decimal(60)),
2357+ ('F', Decimal('0.0'), Decimal(0))],
2358+ 'A+', 'D-')
2359
2360
2361 class RangedValuesScoreSystem(AbstractValuesScoreSystem):
2362@@ -271,6 +309,7 @@
2363 value = self.getNumericalValue(score) - self.min
2364 return value / (self.max - self.min)
2365
2366+
2367 class GlobalRangedValuesScoreSystem(RangedValuesScoreSystem):
2368
2369 def __init__(self, name, *args, **kwargs):
2370@@ -294,9 +333,61 @@
2371 class ICustomScoreSystem(zope.interface.Interface):
2372 """Marker interface for score systems created in the widget."""
2373
2374+
2375 class IScoreSystemField(zope.schema.interfaces.IField):
2376 """A field that represents score system."""
2377
2378+
2379 class ScoreSystemField(zope.schema.Field):
2380 """Score System Field."""
2381 zope.interface.implements(IScoreSystemField)
2382+
2383+
2384+class CustomScoreSystem(DiscreteValuesScoreSystem, Persistent):
2385+ """ScoreSystem class for custom (user-created) score systems"""
2386+
2387+ implements(interfaces.ICustomScoreSystem)
2388+
2389+
2390+class ScoreSystemsProxy(object):
2391+ """The Proxy class for adding/editing score systems"""
2392+
2393+ implements(IScoreSystemsProxy)
2394+ adapts(ISchoolToolApplication)
2395+
2396+ def __init__(self, app):
2397+ self.app = app
2398+ self.siteManager = app.getSiteManager()
2399+ self.__parent__ = app
2400+ self.__name__ = 'scoresystems'
2401+
2402+ def getScoreSystems(self):
2403+ """Return list of tuples (name, scoresystem)"""
2404+ results = []
2405+ for name, util in sorted(self.siteManager.getUtilitiesFor(
2406+ interfaces.ICustomScoreSystem)):
2407+ util = removeSecurityProxy(util)
2408+ if util.hidden:
2409+ continue
2410+ results.append((name, util))
2411+ return results
2412+
2413+ def addScoreSystem(self, scoresystem):
2414+ """Add scoresystem to app utilitiles"""
2415+ names = [name for name, util in self.getScoreSystems()]
2416+ name = scoresystem.title
2417+ n = name
2418+ i = 1
2419+ while n in names:
2420+ n = name + u'-' + unicode(i)
2421+ i += 1
2422+ self.siteManager.registerUtility(scoresystem,
2423+ interfaces.ICustomScoreSystem, name=n)
2424+
2425+ def getScoreSystem(self, name):
2426+ """Get scoresystem from app utilitiles by the given name"""
2427+ for n, util in self.getScoreSystems():
2428+ if n == name:
2429+ return removeSecurityProxy(util)
2430+ raise KeyError
2431+
2432
2433=== modified file 'src/schooltool/requirement/tests.py'
2434--- src/schooltool/requirement/tests.py 2006-01-15 23:50:25 +0000
2435+++ src/schooltool/requirement/tests.py 2009-05-08 18:19:21 +0000
2436@@ -47,12 +47,14 @@
2437 setUp=setUp, tearDown=tearDown,
2438 globs={'pprint': doctestunit.pprint},
2439 optionflags=doctest.NORMALIZE_WHITESPACE|
2440- doctest.ELLIPSIS),
2441+ doctest.ELLIPSIS|
2442+ doctest.REPORT_ONLY_FIRST_FAILURE),
2443 doctest.DocFileSuite('grades.txt',
2444 setUp=setUp, tearDown=tearDown,
2445 globs={'pprint': doctestunit.pprint},
2446 optionflags=doctest.NORMALIZE_WHITESPACE|
2447- doctest.ELLIPSIS),
2448+ doctest.ELLIPSIS|
2449+ doctest.REPORT_ONLY_FIRST_FAILURE),
2450 ))
2451
2452 if __name__ == '__main__':

Subscribers

People subscribed via source and target branches