2
# SchoolTool - common information systems platform for school administration
3
# Copyright (c) 2005 Shuttleworth Foundation
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
__docformat__ = 'reStructuredText'
27
from decimal import Decimal
28
from StringIO import StringIO
31
from zope.container.interfaces import INameChooser
32
from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
33
from zope.component import queryUtility
34
from zope.html.field import HtmlFragment
35
from zope.publisher.browser import BrowserView
36
from zope.schema import ValidationError, TextLine
37
from zope.schema.interfaces import IVocabularyFactory
38
from zope.security import proxy
39
from zope.traversing.api import getName
40
from zope.traversing.browser.absoluteurl import absoluteURL
41
from zope.viewlet import viewlet
42
from zope.i18n.interfaces.locales import ICollator
44
from z3c.form import form as z3cform
45
from z3c.form import field, button
47
from schooltool.app.interfaces import ISchoolToolApplication
48
from schooltool.course.interfaces import ISection, ISectionContainer
49
from schooltool.course.interfaces import ILearner, IInstructor
50
from schooltool.gradebook import interfaces
51
from schooltool.gradebook.activity import ensureAtLeastOneWorksheet
52
from schooltool.gradebook.activity import createSourceString, getSourceObj
53
from schooltool.gradebook.activity import Worksheet, LinkedColumnActivity
54
from schooltool.gradebook.browser.report_utils import buildHTMLParagraphs
55
from schooltool.gradebook.gradebook import (getCurrentSectionTaught,
56
setCurrentSectionTaught, getCurrentSectionAttended,
57
setCurrentSectionAttended)
58
from schooltool.person.interfaces import IPerson
59
from schooltool.requirement.scoresystem import UNSCORED
60
from schooltool.requirement.interfaces import ICommentScoreSystem
61
from schooltool.requirement.interfaces import IValuesScoreSystem
62
from schooltool.requirement.interfaces import IDiscreteValuesScoreSystem
63
from schooltool.requirement.interfaces import IRangedValuesScoreSystem
64
from schooltool.schoolyear.interfaces import ISchoolYear, ISchoolYearContainer
65
from schooltool.table.table import simple_form_key
66
from schooltool.term.interfaces import ITerm
68
from schooltool.gradebook import GradebookMessage as _
71
GradebookCSSViewlet = viewlet.CSSViewlet("gradebook.css")
73
DISCRETE_SCORE_SYSTEM = 'd'
74
RANGED_SCORE_SYSTEM = 'r'
75
COMMENT_SCORE_SYSTEM = 'c'
76
SUMMARY_TITLE = _('Summary')
78
column_keys = [('total', _("Total")), ('average', _("Ave."))]
82
"""converts title-based scoresystem name to querystring format"""
83
chars = [c for c in name.lower() if c.isalnum() or c == ' ']
84
return u''.join(chars).replace(' ', '-')
87
def getScoreSystemFromEscName(name):
88
"""converts escaped scoresystem title to scoresystem"""
89
factory = queryUtility(IVocabularyFactory,
90
'schooltool.requirement.discretescoresystems')
93
if name == escName(term.token):
98
def convertAverage(average, scoresystem):
99
"""converts average to display value of the given scoresystem"""
100
if scoresystem is None:
101
return '%.1f%%' % average
102
for score in scoresystem.scores:
103
if average >= score[3]:
107
class GradebookStartup(object):
108
"""A view for entry into into the gradebook or mygrades views."""
111
if IPerson(self.request.principal, None) is None:
112
url = absoluteURL(ISchoolToolApplication(None), self.request)
113
url = '%s/auth/@@login.html?nexturl=%s' % (url, self.request.URL)
114
self.request.response.redirect(url)
116
template = ViewPageTemplateFile('gradebook_startup.pt')
117
return template(self)
120
self.person = IPerson(self.request.principal)
121
self.sectionsTaught = list(IInstructor(self.person).sections())
122
self.sectionsAttended = list(ILearner(self.person).sections())
124
if self.sectionsTaught:
125
section = getCurrentSectionTaught(self.person)
126
if section is None or section.__parent__ is None:
127
section = self.sectionsTaught[0]
128
self.gradebookURL = absoluteURL(section, self.request)+ '/gradebook'
129
if not self.sectionsAttended:
130
self.request.response.redirect(self.gradebookURL)
131
if self.sectionsAttended:
132
section = getCurrentSectionAttended(self.person)
133
if section is None or section.__parent__ is None:
134
section = self.sectionsAttended[0]
135
self.mygradesURL = absoluteURL(section, self.request) + '/mygrades'
136
if not self.sectionsTaught:
137
self.request.response.redirect(self.mygradesURL)
140
class SectionGradebookRedirectView(BrowserView):
141
"""A view for redirecting from a section to either the gradebook for its
142
current worksheet or the final grades view for the section.
143
In the case of final grades for the section, the query string,
144
?final=yes is used to isntruct this view to redirect to the final grades
145
view instead of the gradebook"""
148
person = IPerson(self.request.principal)
149
activities = interfaces.IActivities(self.context)
150
ensureAtLeastOneWorksheet(activities)
151
current_worksheet = activities.getCurrentWorksheet(person)
152
url = absoluteURL(activities, self.request)
153
if current_worksheet is not None:
154
url = absoluteURL(current_worksheet, self.request)
155
if person in self.context.members:
159
if 'final' in self.request:
161
self.request.response.redirect(url)
162
return "Redirecting..."
165
class GradebookBase(BrowserView):
167
def __init__(self, context, request):
168
super(GradebookBase, self).__init__(context, request)
173
t = datetime.datetime.now()
174
return "%s-%s-%s %s:%s:%s" % (t.year, t.month, t.day,
175
t.hour, t.minute, t.second)
179
return self.context.students
184
person = IPerson(self.request.principal)
185
gradebook = proxy.removeSecurityProxy(self.context)
186
worksheet = gradebook.getCurrentWorksheet(person)
187
for activity in gradebook.getWorksheetActivities(worksheet):
188
if interfaces.ILinkedColumnActivity.providedBy(activity):
190
ss = activity.scoresystem
191
if IDiscreteValuesScoreSystem.providedBy(ss):
192
result = [DISCRETE_SCORE_SYSTEM] + [score[0]
193
for score in ss.scores]
194
elif IRangedValuesScoreSystem.providedBy(ss):
195
result = [RANGED_SCORE_SYSTEM, ss.min, ss.max]
197
result = [COMMENT_SCORE_SYSTEM]
198
resultStr = ', '.join(["'%s'" % unicode(value)
199
for value in result])
200
results[activity.__name__] = resultStr
203
def breakJSString(self, origstr):
204
newstr = unicode(origstr)
205
newstr = newstr.replace('\n', '')
206
newstr = newstr.replace('\r', '')
207
newstr = "\\'".join(newstr.split("'"))
208
newstr = '\\"'.join(newstr.split('"'))
212
def warningText(self):
213
return _('You have some changes that have not been saved. Click OK to save now or CANCEL to continue without saving.')
216
class SectionFinder(GradebookBase):
217
"""Base class for GradebookOverview and MyGradesView"""
219
def getUserSections(self):
221
return list(IInstructor(self.person).sections())
223
return list(ILearner(self.person).sections())
225
def getTermId(self, term):
226
year = ISchoolYear(term)
227
return '%s.%s' % (simple_form_key(year), simple_form_key(term))
230
currentSection = ISection(proxy.removeSecurityProxy(self.context))
231
currentTerm = ITerm(currentSection)
233
for section in self.getUserSections():
234
term = ITerm(section)
235
if term not in terms:
237
return [{'title': '%s / %s' % (ISchoolYear(term).title, term.title),
238
'form_id': self.getTermId(term),
239
'selected': term is currentTerm and 'selected' or None}
242
def getSections(self):
243
currentSection = ISection(proxy.removeSecurityProxy(self.context))
244
currentTerm = ITerm(currentSection)
245
for section in self.getUserSections():
246
term = ITerm(section)
247
if term != currentTerm:
249
url = absoluteURL(section, self.request)
254
title = '%s - %s' % (", ".join([course.title
255
for course in section.courses]),
257
css = 'inactive-menu-item'
258
if section == currentSection:
259
css = 'active-menu-item'
260
yield {'obj': section, 'url': url, 'title': title, 'css': css}
263
def worksheets(self):
265
for worksheet in self.context.worksheets:
266
url = absoluteURL(worksheet, self.request)
272
'title': worksheet.title[:15],
274
'current': worksheet == self.getCurrentWorksheet(),
276
results.append(result)
279
def getCurrentSection(self):
280
section = ISection(proxy.removeSecurityProxy(self.context))
281
return '%s - %s' % (", ".join([course.title
282
for course in section.courses]),
285
def getCurrentTerm(self):
286
section = ISection(proxy.removeSecurityProxy(self.context))
287
term = ITerm(section)
288
return '%s / %s' % (ISchoolYear(term).title, term.title)
290
def handleTermChange(self):
291
if 'currentTerm' in self.request:
292
currentSection = ISection(proxy.removeSecurityProxy(self.context))
294
currentCourse = list(currentSection.courses)[0]
295
except (IndexError,):
297
currentTerm = ITerm(currentSection)
298
requestTermId = self.request['currentTerm']
299
if requestTermId != self.getTermId(currentTerm):
301
for section in self.getUserSections():
302
term = ITerm(section)
303
if self.getTermId(term) == requestTermId:
305
temp = list(section.courses)[0]
306
except (IndexError,):
308
if currentCourse == temp:
311
if newSection is None:
313
url = absoluteURL(newSection, self.request)
318
self.request.response.redirect(url)
322
def handleSectionChange(self):
323
gradebook = proxy.removeSecurityProxy(self.context)
324
if 'currentSection' in self.request:
325
for section in self.getSections():
326
if section['title'] == self.request['currentSection']:
327
if section['obj'] == ISection(gradebook):
329
self.request.response.redirect(section['url'])
333
def processColumnPreferences(self):
334
gradebook = proxy.removeSecurityProxy(self.context)
338
section = ISection(gradebook)
339
instructors = list(section.instructors)
340
if len(instructors) == 0:
343
person = instructors[0]
345
columnPreferences = {}
347
columnPreferences = gradebook.getColumnPreferences(person)
348
column_keys_dict = dict(column_keys)
350
prefs = columnPreferences.get('total', {})
351
self.total_hide = prefs.get('hide', False)
352
self.total_label = prefs.get('label', '')
353
if len(self.total_label) == 0:
354
self.total_label = column_keys_dict['total']
356
prefs = columnPreferences.get('average', {})
357
self.average_hide = prefs.get('hide', False)
358
self.average_label = prefs.get('label', '')
359
if len(self.average_label) == 0:
360
self.average_label = column_keys_dict['average']
361
self.average_scoresystem = getScoreSystemFromEscName(
362
prefs.get('scoresystem', ''))
364
prefs = columnPreferences.get('due_date', {})
365
self.due_date_hide = prefs.get('hide', False)
367
self.apply_all_colspan = 1
368
if gradebook.context.deployed:
369
self.total_hide = True
370
self.average_hide = True
371
if not self.total_hide:
372
self.apply_all_colspan += 1
373
if not self.average_hide:
374
self.apply_all_colspan += 1
377
class GradebookOverview(SectionFinder):
378
"""Gradebook Overview/Table"""
383
self.person = IPerson(self.request.principal)
384
gradebook = proxy.removeSecurityProxy(self.context)
387
"""Make sure the current worksheet matches the current url"""
388
worksheet = gradebook.context
389
gradebook.setCurrentWorksheet(self.person, worksheet)
390
setCurrentSectionTaught(self.person, gradebook.section)
392
"""Retrieve column preferences."""
393
self.processColumnPreferences()
395
"""Retrieve sorting information and store changes of it."""
396
if 'sort_by' in self.request:
397
sort_by = self.request['sort_by']
398
key, reverse = gradebook.getSortKey(self.person)
400
reverse = not reverse
403
gradebook.setSortKey(self.person, (sort_by, reverse))
404
self.sortKey = gradebook.getSortKey(self.person)
406
"""Handle change of current term."""
407
if self.handleTermChange():
410
"""Handle change of current section."""
411
if self.handleSectionChange():
414
"""Handle changes to due date filter"""
415
if 'num_weeks' in self.request:
416
flag, weeks = gradebook.getDueDateFilter(self.person)
417
if 'due_date' in self.request:
421
weeks = self.request['num_weeks']
422
gradebook.setDueDateFilter(self.person, flag, weeks)
424
"""Handle changes to scores."""
425
evaluator = getName(IPerson(self.request.principal))
426
for student in self.context.students:
427
for activity in gradebook.activities:
428
# Create a hash and see whether it is in the request
429
act_hash = activity.__name__
430
cell_name = '%s_%s' % (act_hash, student.username)
431
if cell_name in self.request:
432
# If a value is present, create an evaluation, if the
435
score = activity.scoresystem.fromUnicode(
436
self.request[cell_name])
437
except (ValidationError, ValueError):
439
'Invalid scores (highlighted in red) were not saved.')
441
value, ss = gradebook.getEvaluation(student, activity)
443
if value is not None and score is UNSCORED:
444
self.context.removeEvaluation(student, activity)
447
elif value is None and score is UNSCORED:
449
# Replace the score or add new one/
450
elif value is None or score != value:
452
self.context.evaluate(
453
student, activity, score, evaluator)
455
def getCurrentWorksheet(self):
456
return self.context.getCurrentWorksheet(self.person)
458
def getDueDateFilter(self):
459
flag, weeks = self.context.getDueDateFilter(self.person)
462
def weeksChoices(self):
463
return [unicode(choice) for choice in range(1, 10)]
465
def getCurrentWeeks(self):
466
flag, weeks = self.context.getDueDateFilter(self.person)
469
def getActivityAttrs(self, activity):
470
shortTitle = activity.label
471
if shortTitle is None or len(shortTitle) == 0:
472
shortTitle = activity.title
473
shortTitle = shortTitle.replace(' ', '')
474
if len(shortTitle) > 5:
475
shortTitle = shortTitle[:5].strip()
476
longTitle = activity.title
477
if ICommentScoreSystem.providedBy(activity.scoresystem):
480
bestScore = activity.scoresystem.getBestScore()
481
return shortTitle, longTitle, bestScore
483
def activities(self):
484
"""Get a list of all activities."""
485
self.person = IPerson(self.request.principal)
487
for activity in self.getFilteredActivities():
488
if interfaces.ILinkedColumnActivity.providedBy(activity):
490
source = getSourceObj(activity.source)
491
if interfaces.IActivity.providedBy(source):
492
shortTitle, longTitle, bestScore = \
493
self.getActivityAttrs(source)
494
if source.label is not None and len(source.label):
495
shortTitle = source.label
496
if source.title is not None and len(source.title):
497
longTitle = source.title
498
elif interfaces.IWorksheet.providedBy(source):
499
shortTitle = source.title
500
if activity.label is not None and len(activity.label):
501
shortTitle = activity.label
502
if len(shortTitle) > 5:
503
shortTitle = shortTitle[:5].strip()
504
longTitle = source.title
507
shortTitle = longTitle = bestScore = ''
509
scorable = not ICommentScoreSystem.providedBy(
510
activity.scoresystem)
511
shortTitle, longTitle, bestScore = \
512
self.getActivityAttrs(activity)
514
'scorable': scorable,
515
'shortTitle': shortTitle,
516
'longTitle': longTitle,
518
'hash': activity.__name__,
520
results.append(result)
523
def scorableActivities(self):
524
"""Get a list of those activities that can be scored."""
525
return [result for result in self.activities() if result['scorable']]
527
def isFiltered(self, activity):
528
if interfaces.ILinkedColumnActivity.providedBy(activity):
530
flag, weeks = self.context.getDueDateFilter(self.person)
533
cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
534
return activity.due_date < cutoff
536
def getFilteredActivities(self):
537
activities = self.context.getCurrentActivities(self.person)
538
return[activity for activity in activities
539
if not self.isFiltered(activity)]
541
def getStudentActivityValue(self, student, activity):
542
gradebook = proxy.removeSecurityProxy(self.context)
543
value, ss = gradebook.getEvaluation(student, activity)
544
if value is None or value is UNSCORED:
547
act_hash = activity.__name__
548
cell_name = '%s_%s' % (act_hash, student.username)
549
if cell_name in self.request:
550
value = self.request[cell_name]
552
if value and ICommentScoreSystem.providedBy(activity.scoresystem):
558
"""Generate the table of grades."""
559
gradebook = proxy.removeSecurityProxy(self.context)
560
worksheet = gradebook.getCurrentWorksheet(self.person)
561
activities = [(activity.__name__, activity)
562
for activity in self.getFilteredActivities()]
564
for student in self.context.students:
566
for act_hash, activity in activities:
567
value = self.getStudentActivityValue(student, activity)
568
if interfaces.ILinkedColumnActivity.providedBy(activity):
570
if value is not UNSCORED and value != '':
571
value = '%.1f' % value
573
editable = not ICommentScoreSystem.providedBy(
574
activity.scoresystem)
577
'activity': act_hash,
578
'editable': editable,
583
total, average = gradebook.getWorksheetTotalAverage(worksheet,
586
total = "%.1f" % total
588
if average is UNSCORED:
591
average = convertAverage(average, self.average_scoresystem)
594
{'student': {'title': student.title,
595
'id': student.username,
596
'url': absoluteURL(student, self.request),
597
'gradeurl': absoluteURL(self.context, self.request) +
598
('/%s' % student.username),
601
'total': unicode(total),
602
'average': unicode(average)
606
key, reverse = self.sortKey
607
self.collator = ICollator(self.request.locale)
608
def generateKey(row):
610
grades = dict([(unicode(grade['activity']), grade['value'])
611
for grade in row['grades']])
612
if not grades.get(key, ''):
613
return (1, self.collator.key(row['student']['title']))
615
return (0, grades.get(key))
616
return self.collator.key(row['student']['title'])
617
return sorted(rows, key=generateKey, reverse=reverse)
620
def descriptions(self):
621
self.person = IPerson(self.request.principal)
623
for activity in self.getFilteredActivities():
624
description = activity.title
626
'act_hash': activity.__name__,
627
'description': self.breakJSString(description),
629
results.append(result)
633
class GradeActivity(object):
634
"""Grading a single activity"""
638
act_hash = self.request['activity']
639
for activity in self.context.activities:
640
if activity.__name__ == act_hash:
641
return {'title': activity.title,
642
'max': activity.scoresystem.getBestScore(),
643
'hash': activity.__name__,
648
gradebook = proxy.removeSecurityProxy(self.context)
649
for student in self.context.students:
650
reqValue = self.request.get(student.username)
651
value, ss = gradebook.getEvaluation(student, self.activity['obj'])
652
if value is None or value is UNSCORED:
653
value = reqValue or ''
655
value = reqValue or value
657
yield {'student': {'title': student.title, 'id': student.username},
662
if 'CANCEL' in self.request:
663
self.request.response.redirect('index.html')
665
elif 'UPDATE_SUBMIT' in self.request:
666
activity = self.activity['obj']
667
evaluator = getName(IPerson(self.request.principal))
668
gradebook = proxy.removeSecurityProxy(self.context)
669
# Iterate through all students
670
for student in self.context.students:
671
id = student.username
672
if id in self.request:
674
# If a value is present, create an evaluation, if the
677
score = activity.scoresystem.fromUnicode(
679
except (ValidationError, ValueError):
681
'The grade $value for $name is not valid.',
682
mapping={'value': self.request[id],
683
'name': student.title})
684
self.messages.append(message)
686
value, ss = gradebook.getEvaluation(student, activity)
688
if value is not None and score is UNSCORED:
689
self.context.removeEvaluation(student, activity)
691
elif value is None and score is UNSCORED:
693
# Replace the score or add new one/
694
elif value is None or score != value:
695
self.context.evaluate(
696
student, activity, score, evaluator)
698
if not len(self.messages):
699
self.request.response.redirect('index.html')
702
def getScoreSystemDiscreteValues(ss):
703
if IDiscreteValuesScoreSystem.providedBy(ss):
704
return (ss.scores[-1][2], ss.scores[0][2])
705
elif IRangedValuesScoreSystem.providedBy(ss):
706
return (ss.min, ss.max)
710
class MyGradesView(SectionFinder):
711
"""Student view of own grades."""
716
self.person = IPerson(self.request.principal)
717
gradebook = proxy.removeSecurityProxy(self.context)
718
worksheet = proxy.removeSecurityProxy(gradebook.context)
720
"""Make sure the current worksheet matches the current url"""
721
worksheet = gradebook.context
722
gradebook.setCurrentWorksheet(self.person, worksheet)
723
setCurrentSectionAttended(self.person, gradebook.section)
725
"""Retrieve column preferences."""
726
self.processColumnPreferences()
730
for activity in self.context.getCurrentActivities(self.person):
731
activity = proxy.removeSecurityProxy(activity)
732
value, ss = self.context.getEvaluation(self.person, activity)
734
if value is not None and value is not UNSCORED:
735
if ICommentScoreSystem.providedBy(ss):
738
'paragraphs': buildHTMLParagraphs(value),
741
elif IValuesScoreSystem.providedBy(ss):
742
s_min, s_max = getScoreSystemDiscreteValues(ss)
743
if IDiscreteValuesScoreSystem.providedBy(ss):
744
value = ss.getNumericalValue(value)
747
count += s_max - s_min
750
'value': '%s / %s' % (value, ss.getBestScore()),
765
title = activity.title
766
if activity.description:
767
title += ' - %s' % activity.description
773
self.table.append(row)
776
total, average = gradebook.getWorksheetTotalAverage(worksheet,
778
self.average = convertAverage(average, self.average_scoresystem)
782
"""Handle change of current term."""
783
if self.handleTermChange():
786
"""Handle change of current section."""
787
self.handleSectionChange()
789
def getCurrentWorksheet(self):
790
return self.context.getCurrentWorksheet(self.person)
793
class LinkedActivityGradesUpdater(object):
794
"""Functionality to update grades from a linked activity"""
796
def update(self, linked_activity, request):
797
evaluator = getName(IPerson(request.principal))
798
external_activity = linked_activity.getExternalActivity()
799
if external_activity is None:
800
msg = "Couldn't find an ExternalActivity match for %s"
801
raise LookupError(msg % external_activity.title)
802
worksheet = linked_activity.__parent__
803
gradebook = interfaces.IGradebook(worksheet)
804
for student in gradebook.students:
805
external_grade = external_activity.getGrade(student)
806
if external_grade is not None:
807
score = Decimal("%.2f" % external_grade) * \
808
Decimal(linked_activity.points)
809
gradebook.evaluate(student, linked_activity, score, evaluator)
812
class UpdateLinkedActivityGrades(LinkedActivityGradesUpdater):
813
"""A view for updating the grades of a linked activity."""
816
self.update(self.context, self.request)
817
next_url = absoluteURL(self.context.__parent__, self.request) + \
819
self.request.response.redirect(next_url)
822
class GradebookColumnPreferences(BrowserView):
823
"""A view for editing a teacher's gradebook column preferences."""
825
def worksheets(self):
827
gradebook = proxy.removeSecurityProxy(self.context)
828
for worksheet in gradebook.context.__parent__.values():
829
if worksheet.deployed:
831
results.append(worksheet)
834
def addSummary(self):
835
gradebook = proxy.removeSecurityProxy(self.context)
836
worksheets = gradebook.context.__parent__
838
overwrite = self.request.get('overwrite', '') == 'on'
840
currentWorksheets = []
841
for worksheet in worksheets.values():
842
if worksheet.deployed:
844
if worksheet.title == SUMMARY_TITLE:
845
while len(worksheet.values()):
846
del worksheet[worksheet.values()[0].__name__]
849
currentWorksheets.append(worksheet)
852
next = self.nextSummaryTitle()
853
currentWorksheets = self.worksheets()
854
summary = Worksheet(next)
855
chooser = INameChooser(worksheets)
856
name = chooser.chooseName('', summary)
857
worksheets[name] = summary
859
for worksheet in currentWorksheets:
860
if worksheet.title.startswith(SUMMARY_TITLE):
862
activity = LinkedColumnActivity(worksheet.title, u'assignment',
863
'', createSourceString(worksheet))
864
chooser = INameChooser(summary)
865
name = chooser.chooseName('', activity)
866
summary[name] = activity
868
def nextSummaryTitle(self):
872
for worksheet in self.worksheets():
873
if worksheet.title == next:
878
next = SUMMARY_TITLE + str(index)
881
def summaryFound(self):
882
return self.nextSummaryTitle() != SUMMARY_TITLE
885
self.person = IPerson(self.request.principal)
886
gradebook = proxy.removeSecurityProxy(self.context)
888
if 'UPDATE_SUBMIT' in self.request:
889
columnPreferences = gradebook.getColumnPreferences(self.person)
890
for key, name in column_keys:
891
prefs = columnPreferences.setdefault(key, {})
892
if 'hide_' + key in self.request:
895
prefs['hide'] = False
896
if 'label_' + key in self.request:
897
prefs['label'] = self.request['label_' + key]
901
prefs['scoresystem'] = self.request['scoresystem_' + key]
902
prefs = columnPreferences.setdefault('due_date', {})
903
if 'hide_due_date' in self.request:
906
prefs['hide'] = False
907
gradebook.setColumnPreferences(self.person, columnPreferences)
909
if 'ADD_SUMMARY' in self.request:
912
if 'form-submitted' in self.request:
913
self.request.response.redirect('index.html')
916
def hide_due_date_value(self):
917
self.person = IPerson(self.request.principal)
918
gradebook = proxy.removeSecurityProxy(self.context)
919
columnPreferences = gradebook.getColumnPreferences(self.person)
920
prefs = columnPreferences.get('due_date', {})
921
return prefs.get('hide', False)
925
self.person = IPerson(self.request.principal)
926
gradebook = proxy.removeSecurityProxy(self.context)
928
columnPreferences = gradebook.getColumnPreferences(self.person)
929
for key, name in column_keys:
930
prefs = columnPreferences.get(key, {})
931
hide = prefs.get('hide', False)
932
label = prefs.get('label', '')
933
scoresystem = prefs.get('scoresystem', '')
936
'hide_name': 'hide_' + key,
938
'label_name': 'label_' + key,
939
'label_value': label,
940
'scoresystem_name': 'scoresystem_' + key,
941
'scoresystem_value': scoresystem,
943
results.append(result)
947
def scoresystems(self):
948
factory = queryUtility(IVocabularyFactory,
949
'schooltool.requirement.discretescoresystems')
950
vocab = factory(None)
952
'name': _('-- No score system --'),
959
'value': escName(term.token),
961
results.append(result)
965
class NoCurrentTerm(BrowserView):
966
"""A view for informing the user of the need to set up a schoolyear
967
and at least one term."""
973
class GradeStudent(z3cform.EditForm):
974
"""Edit form for a student's grades."""
975
z3cform.extends(z3cform.EditForm)
976
template = ViewPageTemplateFile('grade_student.pt')
978
def __init__(self, context, request):
979
super(GradeStudent, self).__init__(context, request)
980
if 'nexturl' in self.request:
981
self.nexturl = self.request['nexturl']
983
self.nexturl = self.gradebookURL()
986
self.person = IPerson(self.request.principal)
987
for index, activity in enumerate(self.getFilteredActivities()):
988
if interfaces.ILinkedColumnActivity.providedBy(activity):
989
obj = getSourceObj(activity.source)
990
newSchemaFld = TextLine(
995
if ICommentScoreSystem.providedBy(activity.scoresystem):
996
field_cls = HtmlFragment
999
newSchemaFld = field_cls(
1000
title=activity.title,
1001
description=activity.description,
1002
constraint=activity.scoresystem.fromUnicode,
1004
newSchemaFld.__name__ = str(activity.__name__)
1005
newSchemaFld.interface = interfaces.IStudentGradebookForm
1006
newFormFld = field.Field(newSchemaFld)
1007
self.fields += field.Fields(newFormFld)
1008
super(GradeStudent, self).update()
1010
@button.buttonAndHandler(_("Previous"))
1011
def handle_previous_action(self, action):
1012
if self.applyData():
1014
prev, next = self.prevNextStudent()
1015
if prev is not None:
1016
url = '%s/%s' % (self.gradebookURL(),
1017
urllib.quote(prev.username.encode('utf-8')))
1018
self.request.response.redirect(url)
1020
@button.buttonAndHandler(_("Next"))
1021
def handle_next_action(self, action):
1022
if self.applyData():
1024
prev, next = self.prevNextStudent()
1025
if next is not None:
1026
url = '%s/%s' % (self.gradebookURL(),
1027
urllib.quote(next.username.encode('utf-8')))
1028
self.request.response.redirect(url)
1030
@button.buttonAndHandler(_("Cancel"))
1031
def handle_cancel_action(self, action):
1032
self.request.response.redirect(self.nexturl)
1034
def applyData(self):
1035
data, errors = self.extractData()
1037
self.status = self.formErrorsMessage
1039
changes = self.applyChanges(data)
1041
self.status = self.successMessage
1043
self.status = self.noChangesMessage
1046
def updateActions(self):
1047
super(GradeStudent, self).updateActions()
1048
self.actions['apply'].addClass('button-ok')
1049
self.actions['previous'].addClass('button-ok')
1050
self.actions['next'].addClass('button-ok')
1051
self.actions['cancel'].addClass('button-cancel')
1053
prev, next = self.prevNextStudent()
1055
del self.actions['previous']
1057
del self.actions['next']
1059
def applyChanges(self, data):
1060
super(GradeStudent, self).applyChanges(data)
1061
self.request.response.redirect(self.nexturl)
1063
def prevNextStudent(self):
1064
gradebook = proxy.removeSecurityProxy(self.context.gradebook)
1065
section = ISection(gradebook)
1066
student = self.context.student
1068
prev, next = None, None
1069
members = [member for name, member in
1070
sorted([(m.last_name + m.first_name, m) for m in section.members])]
1071
if len(members) < 2:
1073
for index, member in enumerate(members):
1074
if member == student:
1077
elif index == len(members) - 1:
1080
prev = members[index - 1]
1081
next = members[index + 1]
1085
def isFiltered(self, activity):
1086
flag, weeks = self.context.gradebook.getDueDateFilter(self.person)
1089
cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
1090
return activity.due_date < cutoff
1092
def getFilteredActivities(self):
1093
gradebook = proxy.removeSecurityProxy(self.context.gradebook)
1094
return[activity for activity in gradebook.context.values()
1095
if not self.isFiltered(activity)]
1099
return _(u'Enter grades for ${fullname}',
1100
mapping={'fullname': self.context.student.title})
1102
def gradebookURL(self):
1103
return absoluteURL(self.context.gradebook, self.request)
1106
class StudentGradebookView(object):
1107
"""View a student gradebook."""
1109
def __init__(self, context, request):
1110
self.context = context
1111
self.request = request
1112
self.person = IPerson(self.request.principal)
1113
gradebook = proxy.removeSecurityProxy(self.context.gradebook)
1116
'worksheet': gradebook.context.title,
1117
'student': '%s %s' % (self.context.student.first_name,
1118
self.context.student.last_name),
1119
'section': '%s - %s' % (", ".join([course.title
1121
gradebook.section.courses]),
1122
gradebook.section.title),
1124
self.title = _('$worksheet for $student in $section', mapping=mapping)
1127
activities = [activity for activity in gradebook.context.values()
1128
if not self.isFiltered(activity)]
1129
for activity in activities:
1130
value, ss = gradebook.getEvaluation(self.context.student, activity)
1131
if value is None or value is UNSCORED:
1133
if ICommentScoreSystem.providedBy(activity.scoresystem):
1136
'paragraphs': buildHTMLParagraphs(value),
1143
block['label'] = activity.title
1144
self.blocks.append(block)
1146
def isFiltered(self, activity):
1147
flag, weeks = self.context.gradebook.getDueDateFilter(self.person)
1150
cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
1151
return activity.due_date < cutoff
1154
class GradebookCSVView(BrowserView):
1157
csvfile = StringIO()
1158
writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL)
1159
row = ['year', 'term', 'section', 'worksheet', 'activity', 'student',
1161
writer.writerow(row)
1162
syc = ISchoolYearContainer(self.context)
1163
for year in syc.values():
1164
for term in year.values():
1165
for section in ISectionContainer(term).values():
1166
self.writeGradebookRows(writer, year, term, section)
1167
return csvfile.getvalue().decode('utf-8')
1169
def writeGradebookRows(self, writer, year, term, section):
1170
activities = interfaces.IActivities(section)
1171
for worksheet in activities.values():
1172
gb = interfaces.IGradebook(worksheet)
1173
for student in gb.students:
1174
for activity in gb.activities:
1175
value, ss = gb.getEvaluation(student, activity)
1178
value = unicode(value).replace('\n', '\\n')
1179
value = value.replace('\r', '\\r')
1180
row = [year.__name__, term.__name__, section.__name__,
1181
worksheet.__name__, activity.__name__,
1182
student.username, value]
1183
row = [item.encode('utf-8') for item in row]
1184
writer.writerow(row)