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

« back to all changes in this revision

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

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# SchoolTool - common information systems platform for school administration
 
3
# Copyright (c) 2005 Shuttleworth Foundation
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
#
 
19
"""
 
20
Gradebook Views
 
21
"""
 
22
 
 
23
__docformat__ = 'reStructuredText'
 
24
 
 
25
import csv
 
26
import datetime
 
27
from decimal import Decimal
 
28
from StringIO import StringIO
 
29
import urllib
 
30
 
 
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
 
43
 
 
44
from z3c.form import form as z3cform
 
45
from z3c.form import field, button
 
46
 
 
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
 
67
 
 
68
from schooltool.gradebook import GradebookMessage as _
 
69
 
 
70
 
 
71
GradebookCSSViewlet = viewlet.CSSViewlet("gradebook.css")
 
72
 
 
73
DISCRETE_SCORE_SYSTEM = 'd'
 
74
RANGED_SCORE_SYSTEM = 'r'
 
75
COMMENT_SCORE_SYSTEM = 'c'
 
76
SUMMARY_TITLE = _('Summary')
 
77
 
 
78
column_keys = [('total', _("Total")), ('average', _("Ave."))]
 
79
 
 
80
 
 
81
def escName(name):
 
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(' ', '-')
 
85
 
 
86
 
 
87
def getScoreSystemFromEscName(name):
 
88
    """converts escaped scoresystem title to scoresystem"""
 
89
    factory = queryUtility(IVocabularyFactory,
 
90
                           'schooltool.requirement.discretescoresystems')
 
91
    vocab = factory(None)
 
92
    for term in vocab:
 
93
        if name == escName(term.token):
 
94
            return term.value
 
95
    return None
 
96
 
 
97
 
 
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]:
 
104
            return score[0]
 
105
 
 
106
 
 
107
class GradebookStartup(object):
 
108
    """A view for entry into into the gradebook or mygrades views."""
 
109
 
 
110
    def __call__(self):
 
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)
 
115
            return ''
 
116
        template = ViewPageTemplateFile('gradebook_startup.pt')
 
117
        return template(self)
 
118
 
 
119
    def update(self):
 
120
        self.person = IPerson(self.request.principal)
 
121
        self.sectionsTaught = list(IInstructor(self.person).sections())
 
122
        self.sectionsAttended = list(ILearner(self.person).sections())
 
123
 
 
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)
 
138
 
 
139
 
 
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"""
 
146
 
 
147
    def __call__(self):
 
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:
 
156
                url += '/mygrades'
 
157
            else:
 
158
                url += '/gradebook'
 
159
            if 'final' in self.request:
 
160
                url += '/final.html'
 
161
        self.request.response.redirect(url)
 
162
        return "Redirecting..."
 
163
 
 
164
 
 
165
class GradebookBase(BrowserView):
 
166
 
 
167
    def __init__(self, context, request):
 
168
        super(GradebookBase, self).__init__(context, request)
 
169
        self.changed = False
 
170
 
 
171
    @property
 
172
    def time(self):
 
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)
 
176
 
 
177
    @property
 
178
    def students(self):
 
179
        return self.context.students
 
180
 
 
181
    @property
 
182
    def scores(self):
 
183
        results = {}
 
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):
 
189
                continue
 
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]
 
196
            else:
 
197
                result = [COMMENT_SCORE_SYSTEM]
 
198
            resultStr = ', '.join(["'%s'" % unicode(value)
 
199
                for value in result])
 
200
            results[activity.__name__] = resultStr
 
201
        return results
 
202
 
 
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('"'))
 
209
        return newstr
 
210
 
 
211
    @property
 
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.')
 
214
 
 
215
 
 
216
class SectionFinder(GradebookBase):
 
217
    """Base class for GradebookOverview and MyGradesView"""
 
218
 
 
219
    def getUserSections(self):
 
220
        if self.isTeacher:
 
221
            return list(IInstructor(self.person).sections())
 
222
        else:
 
223
            return list(ILearner(self.person).sections())
 
224
 
 
225
    def getTermId(self, term):
 
226
        year = ISchoolYear(term)
 
227
        return '%s.%s' % (simple_form_key(year), simple_form_key(term))
 
228
 
 
229
    def getTerms(self):
 
230
        currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
231
        currentTerm = ITerm(currentSection)
 
232
        terms = []
 
233
        for section in self.getUserSections():
 
234
            term = ITerm(section)
 
235
            if term not in terms:
 
236
                terms.append(term)
 
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}
 
240
                for term in terms]
 
241
 
 
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:
 
248
                continue
 
249
            url = absoluteURL(section, self.request)
 
250
            if self.isTeacher:
 
251
                url += '/gradebook'
 
252
            else:
 
253
                url += '/mygrades'
 
254
            title = '%s - %s' % (", ".join([course.title
 
255
                                            for course in section.courses]),
 
256
                                 section.title)
 
257
            css = 'inactive-menu-item'
 
258
            if section == currentSection:
 
259
                css = 'active-menu-item'
 
260
            yield {'obj': section, 'url': url, 'title': title, 'css': css}
 
261
 
 
262
    @property
 
263
    def worksheets(self):
 
264
        results = []
 
265
        for worksheet in self.context.worksheets:
 
266
            url = absoluteURL(worksheet, self.request)
 
267
            if self.isTeacher:
 
268
                url += '/gradebook'
 
269
            else:
 
270
                url += '/mygrades'
 
271
            result = {
 
272
                'title': worksheet.title[:15],
 
273
                'url': url,
 
274
                'current': worksheet == self.getCurrentWorksheet(),
 
275
                }
 
276
            results.append(result)
 
277
        return results
 
278
 
 
279
    def getCurrentSection(self):
 
280
        section = ISection(proxy.removeSecurityProxy(self.context))
 
281
        return '%s - %s' % (", ".join([course.title
 
282
                                       for course in section.courses]),
 
283
                            section.title)
 
284
 
 
285
    def getCurrentTerm(self):
 
286
        section = ISection(proxy.removeSecurityProxy(self.context))
 
287
        term = ITerm(section)
 
288
        return '%s / %s' % (ISchoolYear(term).title, term.title)
 
289
 
 
290
    def handleTermChange(self):
 
291
        if 'currentTerm' in self.request:
 
292
            currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
293
            try:
 
294
                currentCourse = list(currentSection.courses)[0]
 
295
            except (IndexError,):
 
296
                currentCourse = None
 
297
            currentTerm = ITerm(currentSection)
 
298
            requestTermId = self.request['currentTerm']
 
299
            if requestTermId != self.getTermId(currentTerm):
 
300
                newSection = None
 
301
                for section in self.getUserSections():
 
302
                    term = ITerm(section)
 
303
                    if self.getTermId(term) == requestTermId:
 
304
                        try:
 
305
                            temp = list(section.courses)[0]
 
306
                        except (IndexError,):
 
307
                            temp = None
 
308
                        if currentCourse == temp:
 
309
                            newSection = section
 
310
                            break
 
311
                        if newSection is None:
 
312
                            newSection = section
 
313
                url = absoluteURL(newSection, self.request)
 
314
                if self.isTeacher:
 
315
                    url += '/gradebook'
 
316
                else:
 
317
                    url += '/mygrades'
 
318
                self.request.response.redirect(url)
 
319
                return True
 
320
        return False
 
321
 
 
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):
 
328
                        break
 
329
                    self.request.response.redirect(section['url'])
 
330
                    return True
 
331
        return False
 
332
 
 
333
    def processColumnPreferences(self):
 
334
        gradebook = proxy.removeSecurityProxy(self.context)
 
335
        if self.isTeacher:
 
336
            person = self.person
 
337
        else:
 
338
            section = ISection(gradebook)
 
339
            instructors = list(section.instructors)
 
340
            if len(instructors) == 0:
 
341
                person = None
 
342
            else:
 
343
                person = instructors[0]
 
344
        if person is None:
 
345
            columnPreferences = {}
 
346
        else:
 
347
            columnPreferences = gradebook.getColumnPreferences(person)
 
348
        column_keys_dict = dict(column_keys)
 
349
 
 
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']
 
355
 
 
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', ''))
 
363
 
 
364
        prefs = columnPreferences.get('due_date', {})
 
365
        self.due_date_hide = prefs.get('hide', False)
 
366
 
 
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
 
375
 
 
376
 
 
377
class GradebookOverview(SectionFinder):
 
378
    """Gradebook Overview/Table"""
 
379
 
 
380
    isTeacher = True
 
381
 
 
382
    def update(self):
 
383
        self.person = IPerson(self.request.principal)
 
384
        gradebook = proxy.removeSecurityProxy(self.context)
 
385
        self.message = ''
 
386
 
 
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)
 
391
 
 
392
        """Retrieve column preferences."""
 
393
        self.processColumnPreferences()
 
394
 
 
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)
 
399
            if sort_by == key:
 
400
                reverse = not reverse
 
401
            else:
 
402
                reverse=False
 
403
            gradebook.setSortKey(self.person, (sort_by, reverse))
 
404
        self.sortKey = gradebook.getSortKey(self.person)
 
405
 
 
406
        """Handle change of current term."""
 
407
        if self.handleTermChange():
 
408
            return
 
409
 
 
410
        """Handle change of current section."""
 
411
        if self.handleSectionChange():
 
412
            return
 
413
 
 
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:
 
418
                flag = True
 
419
            else:
 
420
                flag = False
 
421
            weeks = self.request['num_weeks']
 
422
            gradebook.setDueDateFilter(self.person, flag, weeks)
 
423
 
 
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
 
433
                    # score is different
 
434
                    try:
 
435
                        score = activity.scoresystem.fromUnicode(
 
436
                            self.request[cell_name])
 
437
                    except (ValidationError, ValueError):
 
438
                        self.message = _(
 
439
                            'Invalid scores (highlighted in red) were not saved.')
 
440
                        continue
 
441
                    value, ss = gradebook.getEvaluation(student, activity)
 
442
                    # Delete the score
 
443
                    if value is not None and score is UNSCORED:
 
444
                        self.context.removeEvaluation(student, activity)
 
445
                        self.changed = True
 
446
                    # Do nothing
 
447
                    elif value is None and score is UNSCORED:
 
448
                        continue
 
449
                    # Replace the score or add new one/
 
450
                    elif value is None or score != value:
 
451
                        self.changed = True
 
452
                        self.context.evaluate(
 
453
                            student, activity, score, evaluator)
 
454
 
 
455
    def getCurrentWorksheet(self):
 
456
        return self.context.getCurrentWorksheet(self.person)
 
457
 
 
458
    def getDueDateFilter(self):
 
459
        flag, weeks = self.context.getDueDateFilter(self.person)
 
460
        return flag
 
461
 
 
462
    def weeksChoices(self):
 
463
        return [unicode(choice) for choice in range(1, 10)]
 
464
 
 
465
    def getCurrentWeeks(self):
 
466
        flag, weeks = self.context.getDueDateFilter(self.person)
 
467
        return weeks
 
468
 
 
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):
 
478
            bestScore = ''
 
479
        else:
 
480
            bestScore = activity.scoresystem.getBestScore()
 
481
        return shortTitle, longTitle, bestScore
 
482
 
 
483
    def activities(self):
 
484
        """Get  a list of all activities."""
 
485
        self.person = IPerson(self.request.principal)
 
486
        results = []
 
487
        for activity in self.getFilteredActivities():
 
488
            if interfaces.ILinkedColumnActivity.providedBy(activity):
 
489
                scorable = False
 
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
 
505
                    bestScore = '100'
 
506
                else:
 
507
                    shortTitle = longTitle = bestScore = ''
 
508
            else:
 
509
                scorable = not ICommentScoreSystem.providedBy(
 
510
                    activity.scoresystem)
 
511
                shortTitle, longTitle, bestScore = \
 
512
                    self.getActivityAttrs(activity)
 
513
            result = {
 
514
                'scorable': scorable,
 
515
                'shortTitle': shortTitle,
 
516
                'longTitle': longTitle,
 
517
                'max': bestScore,
 
518
                'hash': activity.__name__,
 
519
                }
 
520
            results.append(result)
 
521
        return results
 
522
 
 
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']]
 
526
 
 
527
    def isFiltered(self, activity):
 
528
        if interfaces.ILinkedColumnActivity.providedBy(activity):
 
529
            return False
 
530
        flag, weeks = self.context.getDueDateFilter(self.person)
 
531
        if not flag:
 
532
            return False
 
533
        cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
 
534
        return activity.due_date < cutoff
 
535
 
 
536
    def getFilteredActivities(self):
 
537
        activities = self.context.getCurrentActivities(self.person)
 
538
        return[activity for activity in activities
 
539
               if not self.isFiltered(activity)]
 
540
 
 
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:
 
545
            value = ''
 
546
 
 
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]
 
551
 
 
552
        if value and ICommentScoreSystem.providedBy(activity.scoresystem):
 
553
            value = '...'
 
554
 
 
555
        return value
 
556
 
 
557
    def table(self):
 
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()]
 
563
        rows = []
 
564
        for student in self.context.students:
 
565
            grades = []
 
566
            for act_hash, activity in activities:
 
567
                value = self.getStudentActivityValue(student, activity)
 
568
                if interfaces.ILinkedColumnActivity.providedBy(activity):
 
569
                    editable = False
 
570
                    if value is not UNSCORED and value != '':
 
571
                        value = '%.1f' % value
 
572
                else:
 
573
                    editable = not ICommentScoreSystem.providedBy(
 
574
                        activity.scoresystem)
 
575
 
 
576
                grade = {
 
577
                    'activity': act_hash,
 
578
                    'editable': editable,
 
579
                    'value': value
 
580
                    }
 
581
                grades.append(grade)
 
582
 
 
583
            total, average = gradebook.getWorksheetTotalAverage(worksheet,
 
584
                student)
 
585
 
 
586
            total = "%.1f" % total
 
587
 
 
588
            if average is UNSCORED:
 
589
                average = _('N/A')
 
590
            else:
 
591
                average = convertAverage(average, self.average_scoresystem)
 
592
 
 
593
            rows.append(
 
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),
 
599
                            },
 
600
                 'grades': grades,
 
601
                 'total': unicode(total),
 
602
                 'average': unicode(average)
 
603
                })
 
604
 
 
605
        # Do the sorting
 
606
        key, reverse = self.sortKey
 
607
        self.collator = ICollator(self.request.locale)
 
608
        def generateKey(row):
 
609
            if key != 'student':
 
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']))
 
614
                else:
 
615
                    return (0, grades.get(key))
 
616
            return self.collator.key(row['student']['title'])
 
617
        return sorted(rows, key=generateKey, reverse=reverse)
 
618
 
 
619
    @property
 
620
    def descriptions(self):
 
621
        self.person = IPerson(self.request.principal)
 
622
        results = []
 
623
        for activity in self.getFilteredActivities():
 
624
            description = activity.title
 
625
            result = {
 
626
                'act_hash': activity.__name__,
 
627
                'description': self.breakJSString(description),
 
628
                }
 
629
            results.append(result)
 
630
        return results
 
631
 
 
632
 
 
633
class GradeActivity(object):
 
634
    """Grading a single activity"""
 
635
 
 
636
    @property
 
637
    def activity(self):
 
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__,
 
644
                         'obj': activity}
 
645
 
 
646
    @property
 
647
    def grades(self):
 
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 ''
 
654
            else:
 
655
                value = reqValue or value
 
656
 
 
657
            yield {'student': {'title': student.title, 'id': student.username},
 
658
                   'value': value}
 
659
 
 
660
    def update(self):
 
661
        self.messages = []
 
662
        if 'CANCEL' in self.request:
 
663
            self.request.response.redirect('index.html')
 
664
 
 
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:
 
673
 
 
674
                    # If a value is present, create an evaluation, if the
 
675
                    # score is different
 
676
                    try:
 
677
                        score = activity.scoresystem.fromUnicode(
 
678
                            self.request[id])
 
679
                    except (ValidationError, ValueError):
 
680
                        message = _(
 
681
                            'The grade $value for $name is not valid.',
 
682
                            mapping={'value': self.request[id],
 
683
                                     'name': student.title})
 
684
                        self.messages.append(message)
 
685
                        continue
 
686
                    value, ss = gradebook.getEvaluation(student, activity)
 
687
                    # Delete the score
 
688
                    if value is not None and score is UNSCORED:
 
689
                        self.context.removeEvaluation(student, activity)
 
690
                    # Do nothing
 
691
                    elif value is None and score is UNSCORED:
 
692
                        continue
 
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)
 
697
 
 
698
            if not len(self.messages):
 
699
                self.request.response.redirect('index.html')
 
700
 
 
701
 
 
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)
 
707
    return (0, 0)
 
708
 
 
709
 
 
710
class MyGradesView(SectionFinder):
 
711
    """Student view of own grades."""
 
712
 
 
713
    isTeacher = False
 
714
 
 
715
    def update(self):
 
716
        self.person = IPerson(self.request.principal)
 
717
        gradebook = proxy.removeSecurityProxy(self.context)
 
718
        worksheet = proxy.removeSecurityProxy(gradebook.context)
 
719
 
 
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)
 
724
 
 
725
        """Retrieve column preferences."""
 
726
        self.processColumnPreferences()
 
727
 
 
728
        self.table = []
 
729
        count = 0
 
730
        for activity in self.context.getCurrentActivities(self.person):
 
731
            activity = proxy.removeSecurityProxy(activity)
 
732
            value, ss = self.context.getEvaluation(self.person, activity)
 
733
 
 
734
            if value is not None and value is not UNSCORED:
 
735
                if ICommentScoreSystem.providedBy(ss):
 
736
                    grade = {
 
737
                        'comment': True,
 
738
                        'paragraphs': buildHTMLParagraphs(value),
 
739
                        }
 
740
 
 
741
                elif IValuesScoreSystem.providedBy(ss):
 
742
                    s_min, s_max = getScoreSystemDiscreteValues(ss)
 
743
                    if IDiscreteValuesScoreSystem.providedBy(ss):
 
744
                        value = ss.getNumericalValue(value)
 
745
                        if value is None:
 
746
                            value = 0
 
747
                    count += s_max - s_min
 
748
                    grade = {
 
749
                        'comment': False,
 
750
                        'value': '%s / %s' % (value, ss.getBestScore()),
 
751
                        }
 
752
 
 
753
                else:
 
754
                    grade = {
 
755
                        'comment': False,
 
756
                        'value': value,
 
757
                        }
 
758
 
 
759
            else:
 
760
                grade = {
 
761
                    'comment': False,
 
762
                    'value': '',
 
763
                    }
 
764
 
 
765
            title = activity.title
 
766
            if activity.description:
 
767
                title += ' - %s' % activity.description
 
768
 
 
769
            row = {
 
770
                'activity': title,
 
771
                'grade': grade,
 
772
                }
 
773
            self.table.append(row)
 
774
 
 
775
        if count:
 
776
            total, average = gradebook.getWorksheetTotalAverage(worksheet,
 
777
                self.person)
 
778
            self.average = convertAverage(average, self.average_scoresystem)
 
779
        else:
 
780
            self.average = None
 
781
 
 
782
        """Handle change of current term."""
 
783
        if self.handleTermChange():
 
784
            return
 
785
 
 
786
        """Handle change of current section."""
 
787
        self.handleSectionChange()
 
788
 
 
789
    def getCurrentWorksheet(self):
 
790
        return self.context.getCurrentWorksheet(self.person)
 
791
 
 
792
 
 
793
class LinkedActivityGradesUpdater(object):
 
794
    """Functionality to update grades from a linked activity"""
 
795
 
 
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)
 
810
 
 
811
 
 
812
class UpdateLinkedActivityGrades(LinkedActivityGradesUpdater):
 
813
    """A view for updating the grades of a linked activity."""
 
814
 
 
815
    def __call__(self):
 
816
        self.update(self.context, self.request)
 
817
        next_url = absoluteURL(self.context.__parent__, self.request) + \
 
818
                   '/gradebook'
 
819
        self.request.response.redirect(next_url)
 
820
 
 
821
 
 
822
class GradebookColumnPreferences(BrowserView):
 
823
    """A view for editing a teacher's gradebook column preferences."""
 
824
 
 
825
    def worksheets(self):
 
826
        results = []
 
827
        gradebook = proxy.removeSecurityProxy(self.context)
 
828
        for worksheet in gradebook.context.__parent__.values():
 
829
            if worksheet.deployed:
 
830
                continue
 
831
            results.append(worksheet)
 
832
        return results
 
833
 
 
834
    def addSummary(self):
 
835
        gradebook = proxy.removeSecurityProxy(self.context)
 
836
        worksheets = gradebook.context.__parent__
 
837
 
 
838
        overwrite = self.request.get('overwrite', '') == 'on'
 
839
        if overwrite:
 
840
            currentWorksheets = []
 
841
            for worksheet in worksheets.values():
 
842
                if worksheet.deployed:
 
843
                    continue
 
844
                if worksheet.title == SUMMARY_TITLE:
 
845
                    while len(worksheet.values()):
 
846
                        del worksheet[worksheet.values()[0].__name__]
 
847
                    summary = worksheet
 
848
                else:
 
849
                    currentWorksheets.append(worksheet)
 
850
            next = SUMMARY_TITLE
 
851
        else:
 
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
 
858
 
 
859
        for worksheet in currentWorksheets:
 
860
            if worksheet.title.startswith(SUMMARY_TITLE):
 
861
                continue
 
862
            activity = LinkedColumnActivity(worksheet.title, u'assignment',
 
863
                '', createSourceString(worksheet))
 
864
            chooser = INameChooser(summary)
 
865
            name = chooser.chooseName('', activity)
 
866
            summary[name] = activity
 
867
 
 
868
    def nextSummaryTitle(self):
 
869
        index = 1
 
870
        next = SUMMARY_TITLE
 
871
        while True:
 
872
            for worksheet in self.worksheets():
 
873
                if worksheet.title == next:
 
874
                    break
 
875
            else:
 
876
                break
 
877
            index += 1
 
878
            next = SUMMARY_TITLE + str(index)
 
879
        return next
 
880
 
 
881
    def summaryFound(self):
 
882
        return self.nextSummaryTitle() != SUMMARY_TITLE
 
883
 
 
884
    def update(self):
 
885
        self.person = IPerson(self.request.principal)
 
886
        gradebook = proxy.removeSecurityProxy(self.context)
 
887
 
 
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:
 
893
                    prefs['hide'] = True
 
894
                else:
 
895
                    prefs['hide'] = False
 
896
                if 'label_' + key in self.request:
 
897
                    prefs['label'] = self.request['label_' + key]
 
898
                else:
 
899
                    prefs['label'] = ''
 
900
                if key != 'total':
 
901
                    prefs['scoresystem'] = self.request['scoresystem_' + key]
 
902
            prefs = columnPreferences.setdefault('due_date', {})
 
903
            if 'hide_due_date' in self.request:
 
904
                prefs['hide'] = True
 
905
            else:
 
906
                prefs['hide'] = False
 
907
            gradebook.setColumnPreferences(self.person, columnPreferences)
 
908
 
 
909
        if 'ADD_SUMMARY' in self.request:
 
910
            self.addSummary()
 
911
 
 
912
        if 'form-submitted' in self.request:
 
913
            self.request.response.redirect('index.html')
 
914
 
 
915
    @property
 
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)
 
922
 
 
923
    @property
 
924
    def columns(self):
 
925
        self.person = IPerson(self.request.principal)
 
926
        gradebook = proxy.removeSecurityProxy(self.context)
 
927
        results = []
 
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', '')
 
934
            result = {
 
935
                'name': name,
 
936
                'hide_name': 'hide_' + key,
 
937
                'hide_value': hide,
 
938
                'label_name': 'label_' + key,
 
939
                'label_value': label,
 
940
                'scoresystem_name': 'scoresystem_' + key,
 
941
                'scoresystem_value': scoresystem,
 
942
                }
 
943
            results.append(result)
 
944
        return results
 
945
 
 
946
    @property
 
947
    def scoresystems(self):
 
948
        factory = queryUtility(IVocabularyFactory,
 
949
                               'schooltool.requirement.discretescoresystems')
 
950
        vocab = factory(None)
 
951
        result = {
 
952
            'name': _('-- No score system --'),
 
953
            'value': '',
 
954
            }
 
955
        results = [result]
 
956
        for term in vocab:
 
957
            result = {
 
958
                'name': term.token,
 
959
                'value': escName(term.token),
 
960
                }
 
961
            results.append(result)
 
962
        return results
 
963
 
 
964
 
 
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."""
 
968
 
 
969
    def update(self):
 
970
        pass
 
971
 
 
972
 
 
973
class GradeStudent(z3cform.EditForm):
 
974
    """Edit form for a student's grades."""
 
975
    z3cform.extends(z3cform.EditForm)
 
976
    template = ViewPageTemplateFile('grade_student.pt')
 
977
 
 
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']
 
982
        else:
 
983
            self.nexturl = self.gradebookURL()
 
984
 
 
985
    def update(self):
 
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(
 
991
                    title=obj.title,
 
992
                    readonly = True,
 
993
                    required=False)
 
994
            else:
 
995
                if ICommentScoreSystem.providedBy(activity.scoresystem):
 
996
                    field_cls = HtmlFragment
 
997
                else:
 
998
                    field_cls = TextLine
 
999
                newSchemaFld = field_cls(
 
1000
                    title=activity.title,
 
1001
                    description=activity.description,
 
1002
                    constraint=activity.scoresystem.fromUnicode,
 
1003
                    required=False)
 
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()
 
1009
 
 
1010
    @button.buttonAndHandler(_("Previous"))
 
1011
    def handle_previous_action(self, action):
 
1012
        if self.applyData():
 
1013
            return
 
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)
 
1019
 
 
1020
    @button.buttonAndHandler(_("Next"))
 
1021
    def handle_next_action(self, action):
 
1022
        if self.applyData():
 
1023
            return
 
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)
 
1029
 
 
1030
    @button.buttonAndHandler(_("Cancel"))
 
1031
    def handle_cancel_action(self, action):
 
1032
        self.request.response.redirect(self.nexturl)
 
1033
 
 
1034
    def applyData(self):
 
1035
        data, errors = self.extractData()
 
1036
        if errors:
 
1037
            self.status = self.formErrorsMessage
 
1038
            return True
 
1039
        changes = self.applyChanges(data)
 
1040
        if changes:
 
1041
            self.status = self.successMessage
 
1042
        else:
 
1043
            self.status = self.noChangesMessage
 
1044
        return False
 
1045
 
 
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')
 
1052
 
 
1053
        prev, next = self.prevNextStudent()
 
1054
        if prev is None:
 
1055
            del self.actions['previous']
 
1056
        if next is None:
 
1057
            del self.actions['next']
 
1058
 
 
1059
    def applyChanges(self, data):
 
1060
        super(GradeStudent, self).applyChanges(data)
 
1061
        self.request.response.redirect(self.nexturl)
 
1062
 
 
1063
    def prevNextStudent(self):
 
1064
        gradebook = proxy.removeSecurityProxy(self.context.gradebook)
 
1065
        section = ISection(gradebook)
 
1066
        student = self.context.student
 
1067
 
 
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:
 
1072
            return prev, next
 
1073
        for index, member in enumerate(members):
 
1074
            if member == student:
 
1075
                if index == 0:
 
1076
                    next = members[1]
 
1077
                elif index == len(members) - 1:
 
1078
                    prev = members[-2]
 
1079
                else:
 
1080
                    prev = members[index - 1]
 
1081
                    next = members[index + 1]
 
1082
                break
 
1083
        return prev, next
 
1084
 
 
1085
    def isFiltered(self, activity):
 
1086
        flag, weeks = self.context.gradebook.getDueDateFilter(self.person)
 
1087
        if not flag:
 
1088
            return False
 
1089
        cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
 
1090
        return activity.due_date < cutoff
 
1091
 
 
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)]
 
1096
 
 
1097
    @property
 
1098
    def label(self):
 
1099
        return _(u'Enter grades for ${fullname}',
 
1100
                 mapping={'fullname': self.context.student.title})
 
1101
 
 
1102
    def gradebookURL(self):
 
1103
        return absoluteURL(self.context.gradebook, self.request)
 
1104
 
 
1105
 
 
1106
class StudentGradebookView(object):
 
1107
    """View a student gradebook."""
 
1108
 
 
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)
 
1114
 
 
1115
        mapping = {
 
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
 
1120
                                               for course in
 
1121
                                               gradebook.section.courses]),
 
1122
                                    gradebook.section.title),
 
1123
            }
 
1124
        self.title = _('$worksheet for $student in $section', mapping=mapping)
 
1125
 
 
1126
        self.blocks = []
 
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:
 
1132
                value = ''
 
1133
            if ICommentScoreSystem.providedBy(activity.scoresystem):
 
1134
                block = {
 
1135
                    'comment': True,
 
1136
                    'paragraphs': buildHTMLParagraphs(value),
 
1137
                    }
 
1138
            else:
 
1139
                block = {
 
1140
                    'comment': False,
 
1141
                    'content': value,
 
1142
                    }
 
1143
            block['label'] = activity.title
 
1144
            self.blocks.append(block)
 
1145
 
 
1146
    def isFiltered(self, activity):
 
1147
        flag, weeks = self.context.gradebook.getDueDateFilter(self.person)
 
1148
        if not flag:
 
1149
            return False
 
1150
        cutoff = datetime.date.today() - datetime.timedelta(7 * int(weeks))
 
1151
        return activity.due_date < cutoff
 
1152
 
 
1153
 
 
1154
class GradebookCSVView(BrowserView):
 
1155
 
 
1156
    def __call__(self):
 
1157
        csvfile = StringIO()
 
1158
        writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL)
 
1159
        row = ['year', 'term', 'section', 'worksheet', 'activity', 'student',
 
1160
               'grade']
 
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')
 
1168
 
 
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)
 
1176
                    if value is None:
 
1177
                        continue
 
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)
 
1185