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

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Gediminas Paulauskas
  • Date: 2011-11-29 20:56:56 UTC
  • mfrom: (1.1.5)
  • Revision ID: package-import@ubuntu.com-20111129205656-cd1r6nlkzsds5h2r
Tags: 2.0.0-0ubuntu2
* Upload to Precise.
* debian/rules: Fix to not install *.po files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
31
31
from zope.container.interfaces import INameChooser
32
32
from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
33
33
from zope.component import queryUtility
 
34
from zope.cachedescriptors.property import Lazy
34
35
from zope.html.field import HtmlFragment
35
36
from zope.publisher.browser import BrowserView
36
37
from zope.schema import ValidationError, TextLine
37
38
from zope.schema.interfaces import IVocabularyFactory
38
39
from zope.security import proxy
 
40
from zope.security.interfaces import Unauthorized
39
41
from zope.traversing.api import getName
40
42
from zope.traversing.browser.absoluteurl import absoluteURL
41
43
from zope.viewlet import viewlet
44
46
from z3c.form import form as z3cform
45
47
from z3c.form import field, button
46
48
 
 
49
import schooltool.skin.flourish.page
 
50
import schooltool.skin.flourish.form
47
51
from schooltool.app.interfaces import ISchoolToolApplication
 
52
from schooltool.common.inlinept import InheritTemplate
 
53
from schooltool.common.inlinept import InlineViewPageTemplate
48
54
from schooltool.course.interfaces import ISection, ISectionContainer
49
55
from schooltool.course.interfaces import ILearner, IInstructor
50
56
from schooltool.gradebook import interfaces
64
70
from schooltool.schoolyear.interfaces import ISchoolYear, ISchoolYearContainer
65
71
from schooltool.table.table import simple_form_key
66
72
from schooltool.term.interfaces import ITerm, IDateManager
 
73
from schooltool.skin import flourish
67
74
 
68
75
from schooltool.gradebook import GradebookMessage as _
69
76
 
97
104
class GradebookStartup(object):
98
105
    """A view for entry into into the gradebook or mygrades views."""
99
106
 
 
107
    template = ViewPageTemplateFile('templates/gradebook_startup.pt')
 
108
 
100
109
    def __call__(self):
101
110
        if IPerson(self.request.principal, None) is None:
102
111
            url = absoluteURL(ISchoolToolApplication(None), self.request)
103
112
            url = '%s/auth/@@login.html?nexturl=%s' % (url, self.request.URL)
104
113
            self.request.response.redirect(url)
105
114
            return ''
106
 
        template = ViewPageTemplateFile('templates/gradebook_startup.pt')
107
 
        return template(self)
 
115
        return self.template()
108
116
 
109
117
    def update(self):
110
118
        self.person = IPerson(self.request.principal)
127
135
                self.request.response.redirect(self.mygradesURL)
128
136
 
129
137
 
 
138
class FlourishGradebookStartup(flourish.page.Page, GradebookStartup):
 
139
 
 
140
    def update(self):
 
141
        if IPerson(self.request.principal, None) is None:
 
142
            raise Unauthorized("user not logged in")
 
143
        GradebookStartup.update(self)
 
144
 
 
145
 
 
146
class GradebookStartupNavLink(flourish.page.LinkViewlet):
 
147
 
 
148
    @property
 
149
    def title(self):
 
150
        person = IPerson(self.request.principal, None)
 
151
        if person is None:
 
152
            return ''
 
153
 
 
154
        sectionsTaught = list(IInstructor(person).sections())
 
155
        sectionsAttended = list(ILearner(person).sections())
 
156
 
 
157
        if (not sectionsTaught and
 
158
            not sectionsAttended):
 
159
            return ''
 
160
 
 
161
        return _('Gradebook')
 
162
 
 
163
    @property
 
164
    def url(self):
 
165
        person = IPerson(self.request.principal, None)
 
166
        if person is None:
 
167
            return ''
 
168
        app = ISchoolToolApplication(None)
 
169
        app_url = absoluteURL(app, self.request)
 
170
        return '%s/gradebook.html' % app_url
 
171
 
 
172
 
130
173
class SectionGradebookRedirectView(BrowserView):
131
174
    """A view for redirecting from a section to either the gradebook for its
132
175
       current worksheet or the final grades view for the section.
242
285
            title = '%s - %s' % (", ".join([course.title
243
286
                                            for course in section.courses]),
244
287
                                 section.title)
245
 
            css = 'inactive-menu-item'
246
 
            if section == currentSection:
247
 
                css = 'active-menu-item'
248
 
            yield {'obj': section, 'url': url, 'title': title, 'css': css,
 
288
            yield {'obj': section, 'url': url, 'title': title,
249
289
                   'form_id': self.getSectionId(section),
250
 
                   'selected': title==self.getCurrentSection() and 'selected' or None}
 
290
                   'selected': section == currentSection and 'selected' or None}
251
291
 
252
292
    @property
253
293
    def worksheets(self):
338
378
            columnPreferences = gradebook.getColumnPreferences(person)
339
379
        column_keys_dict = dict(getColumnKeys(gradebook))
340
380
 
 
381
        journal_data = interfaces.ISectionJournalData(ISection(gradebook), None)
341
382
        prefs = columnPreferences.get('absences', {})
342
 
        self.absences_hide = prefs.get('hide', True)
 
383
        if journal_data is None:
 
384
            self.absences_hide = True
 
385
        else:
 
386
            self.absences_hide = prefs.get('hide', True)
343
387
        self.absences_label = prefs.get('label', '')
344
388
        if len(self.absences_label) == 0:
345
389
            self.absences_label = column_keys_dict.get('absences')
346
390
 
347
391
        prefs = columnPreferences.get('tardies', {})
348
 
        self.tardies_hide = prefs.get('hide', True)
 
392
        if journal_data is None:
 
393
            self.tardies_hide = True
 
394
        else:
 
395
            self.tardies_hide = prefs.get('hide', True)
349
396
        self.tardies_label = prefs.get('label', '')
350
397
        if len(self.tardies_label) == 0:
351
398
            self.tardies_label = column_keys_dict.get('tardies')
386
433
 
387
434
    isTeacher = True
388
435
 
 
436
    @Lazy
 
437
    def students_info(self):
 
438
        result = []
 
439
        for student in self.context.students:
 
440
            insecure_student = proxy.removeSecurityProxy(student)
 
441
            result.append({
 
442
                    'title': student.title,
 
443
                    'username': insecure_student.username,
 
444
                    'id': insecure_student.username,
 
445
                    'url': absoluteURL(student, self.request),
 
446
                    'gradeurl': '%s/%s' % (
 
447
                        absoluteURL(self.context, self.request),
 
448
                        insecure_student.username),
 
449
                    'object': student,
 
450
                    })
 
451
        return result
 
452
 
389
453
    def update(self):
390
454
        self.person = IPerson(self.request.principal)
391
455
        gradebook = proxy.removeSecurityProxy(self.context)
406
470
            if sort_by == key:
407
471
                reverse = not reverse
408
472
            else:
409
 
                reverse=False
 
473
                reverse = False
410
474
            gradebook.setSortKey(self.person, (sort_by, reverse))
411
475
        self.sortKey = gradebook.getSortKey(self.person)
412
476
 
430
494
 
431
495
        """Handle changes to scores."""
432
496
        evaluator = getName(IPerson(self.request.principal))
433
 
        for student in self.context.students:
 
497
        for student in self.students_info:
434
498
            for activity in gradebook.activities:
435
499
                # Create a hash and see whether it is in the request
436
500
                act_hash = activity.__name__
437
 
                cell_name = '%s_%s' % (act_hash, student.username)
 
501
                cell_name = '%s_%s' % (act_hash, student['username'])
438
502
                if cell_name in self.request:
439
503
                    # XXX: TODO: clean up this mess.
440
504
                    #      The details of when to remove the evaluation, etc.
451
515
                        self.message = _(
452
516
                            'Invalid scores (highlighted in red) were not saved.')
453
517
                        continue
454
 
                    score = gradebook.getScore(student, activity)
 
518
                    score = gradebook.getScore(student['object'], activity)
455
519
                    # Delete the score
456
520
                    if score and cell_score_value is UNSCORED:
457
 
                        self.context.removeEvaluation(student, activity)
 
521
                        self.context.removeEvaluation(student['object'], activity)
458
522
                        self.changed = True
459
523
                    # Do nothing
460
524
                    elif not score and cell_score_value is UNSCORED:
463
527
                    elif not score or cell_score_value != score.value:
464
528
                        self.changed = True
465
529
                        self.context.evaluate(
466
 
                            student, activity, cell_score_value, evaluator)
 
530
                            student['object'], activity, cell_score_value, evaluator)
467
531
 
468
532
    def getCurrentWorksheet(self):
469
533
        return self.context.getCurrentWorksheet(self.person)
479
543
        flag, weeks = self.context.getDueDateFilter(self.person)
480
544
        return weeks
481
545
 
 
546
    def getLinkedActivityInfo(self, activity):
 
547
        source = getSourceObj(activity.source)
 
548
        insecure_activity = proxy.removeSecurityProxy(activity)
 
549
        insecure_source = proxy.removeSecurityProxy(source)
 
550
 
 
551
        if interfaces.IActivity.providedBy(insecure_source):
 
552
            short, long, best_score = self.getActivityAttrs(source)
 
553
        elif interfaces.IWorksheet.providedBy(insecure_source):
 
554
            long = source.title
 
555
            short = activity.label or long
 
556
            if len(short) > 5:
 
557
                short = short[:5].strip()
 
558
            best_score = '100'
 
559
        else:
 
560
            short = long = best_score = ''
 
561
 
 
562
        return {
 
563
            'linked_source': source,
 
564
            'scorable': False,
 
565
            'shortTitle': short,
 
566
            'longTitle': long,
 
567
            'max': best_score,
 
568
            'hash': insecure_activity.__name__,
 
569
            'object': activity,
 
570
            'updateGrades': '',
 
571
            }
 
572
 
 
573
    def getActivityInfo(self, activity):
 
574
        insecure_activity = proxy.removeSecurityProxy(activity)
 
575
 
 
576
        if interfaces.ILinkedColumnActivity.providedBy(insecure_activity):
 
577
            return self.getLinkedActivityInfo(activity)
 
578
 
 
579
        short, long, best_score = self.getActivityAttrs(activity)
 
580
 
 
581
        scorable = not (
 
582
            ICommentScoreSystem.providedBy(insecure_activity.scoresystem) or
 
583
            interfaces.ILinkedActivity.providedBy(insecure_activity))
 
584
 
 
585
        if interfaces.ILinkedActivity.providedBy(insecure_activity):
 
586
            updateGrades = '%s/updateGrades.html' % (
 
587
                absoluteURL(insecure_activity, self.request))
 
588
        else:
 
589
            updateGrades = ''
 
590
 
 
591
        return {
 
592
            'linked_source': None,
 
593
            'scorable': scorable,
 
594
            'shortTitle': short,
 
595
            'longTitle': long,
 
596
            'max': best_score,
 
597
            'hash': insecure_activity.__name__,
 
598
            'object': activity,
 
599
            'updateGrades': updateGrades,
 
600
            }
 
601
 
 
602
    @Lazy
 
603
    def filtered_activity_info(self):
 
604
        result = []
 
605
        for activity in self.getFilteredActivities():
 
606
            info = self.getActivityInfo(activity)
 
607
            result.append(info)
 
608
        return result
 
609
 
482
610
    def getActivityAttrs(self, activity):
483
 
        shortTitle = activity.label or activity.title
 
611
        longTitle = activity.title
 
612
        shortTitle = activity.label or longTitle
484
613
        shortTitle = shortTitle.replace(' ', '')
485
614
        if len(shortTitle) > 5:
486
615
            shortTitle = shortTitle[:5].strip()
487
 
        longTitle = activity.title
488
 
        if ICommentScoreSystem.providedBy(activity.scoresystem):
 
616
        ss = proxy.removeSecurityProxy(activity.scoresystem)
 
617
        if ICommentScoreSystem.providedBy(ss):
489
618
            bestScore = ''
490
619
        else:
491
 
            bestScore = activity.scoresystem.getBestScore()
 
620
            bestScore = ss.getBestScore()
492
621
        return shortTitle, longTitle, bestScore
493
622
 
494
623
    def activities(self):
495
624
        """Get  a list of all activities."""
496
625
        self.person = IPerson(self.request.principal)
497
626
        results = []
498
 
        for activity in self.getFilteredActivities():
499
 
            if interfaces.ILinkedColumnActivity.providedBy(activity):
500
 
                scorable = False
501
 
                source = getSourceObj(activity.source)
502
 
                if interfaces.IActivity.providedBy(source):
503
 
                    shortTitle, longTitle, bestScore = \
504
 
                        self.getActivityAttrs(source)
505
 
                elif interfaces.IWorksheet.providedBy(source):
506
 
                    shortTitle = activity.label or source.title
507
 
                    if len(shortTitle) > 5:
508
 
                        shortTitle = shortTitle[:5].strip()
509
 
                    longTitle = source.title
510
 
                    bestScore = '100'
511
 
                else:
512
 
                    shortTitle = longTitle = bestScore = ''
513
 
            else:
514
 
                scorable = not (
515
 
                    ICommentScoreSystem.providedBy(activity.scoresystem) or
516
 
                    interfaces.ILinkedActivity.providedBy(activity))
517
 
                shortTitle, longTitle, bestScore = \
518
 
                    self.getActivityAttrs(activity)
519
 
            result = {
520
 
                'scorable': scorable,
521
 
                'shortTitle': shortTitle,
522
 
                'longTitle': longTitle,
523
 
                'max': bestScore,
524
 
                'hash': activity.__name__,
525
 
                }
 
627
        deployed = proxy.removeSecurityProxy(self.context).context.deployed
 
628
        for activity_info in self.filtered_activity_info:
 
629
            result = dict(activity_info)
 
630
            result.update({
 
631
                'canDelete': not deployed,
 
632
                'moveLeft': not deployed,
 
633
                'moveRight': not deployed,
 
634
                })
526
635
            results.append(result)
 
636
        if results:
 
637
            results[0]['moveLeft'] = False
 
638
            results[-1]['moveRight'] = False
527
639
        return results
528
640
 
529
641
    def scorableActivities(self):
545
657
        return[activity for activity in activities
546
658
               if not self.isFiltered(activity)]
547
659
 
548
 
    def getStudentActivityValue(self, student, activity):
 
660
    def getStudentActivityValue(self, student_info, activity):
549
661
        gradebook = proxy.removeSecurityProxy(self.context)
550
 
        score = gradebook.getScore(student, activity)
 
662
        score = gradebook.getScore(student_info['object'], activity)
551
663
        if not score:
552
664
            value = ''
553
665
        else:
554
666
            value = score.value
555
667
        act_hash = activity.__name__
556
 
        cell_name = '%s_%s' % (act_hash, student.username)
 
668
        cell_name = '%s_%s' % (act_hash, student_info['username'])
557
669
        if cell_name in self.request:
558
670
            value = self.request[cell_name]
559
671
 
567
679
        gradebook = proxy.removeSecurityProxy(self.context)
568
680
        worksheet = gradebook.getCurrentWorksheet(self.person)
569
681
        section = ISection(worksheet)
570
 
        activities = [(activity.__name__, activity)
571
 
            for activity in self.getFilteredActivities()]
 
682
 
572
683
        journal_data = interfaces.ISectionJournalData(section, None)
573
684
        rows = []
574
 
        for student in self.context.students:
 
685
        for student_info in self.students_info:
575
686
            grades = []
576
 
            for act_hash, activity in activities:
577
 
                value = self.getStudentActivityValue(student, activity)
578
 
                if interfaces.ILinkedColumnActivity.providedBy(activity):
579
 
                    editable = False
580
 
                    sourceObj = getSourceObj(activity.source)
581
 
                    if value and interfaces.IWorksheet.providedBy(sourceObj):
 
687
            for activity_info in self.filtered_activity_info:
 
688
                activity = activity_info['object']
 
689
                value = self.getStudentActivityValue(student_info, activity)
 
690
                source = activity_info['linked_source']
 
691
                if source is not None:
 
692
                    if value and interfaces.IWorksheet.providedBy(source):
582
693
                        value = '%.1f' % value
583
 
                else:
584
 
                    editable = not (
585
 
                        ICommentScoreSystem.providedBy(activity.scoresystem) or
586
 
                        interfaces.ILinkedActivity.providedBy(activity))
587
 
 
588
694
                grade = {
589
 
                    'activity': act_hash,
590
 
                    'editable': editable,
 
695
                    'activity': activity_info['hash'],
 
696
                    'editable': activity_info['scorable'],
591
697
                    'value': value
592
698
                    }
593
699
                grades.append(grade)
594
700
 
595
 
            total, average = gradebook.getWorksheetTotalAverage(worksheet,
596
 
                student)
 
701
            total, raw_average = gradebook.getWorksheetTotalAverage(
 
702
                worksheet, student_info['object'])
597
703
 
598
704
            total = "%.1f" % total
599
705
 
600
 
            if average is UNSCORED:
 
706
            if raw_average is UNSCORED:
601
707
                average = _('N/A')
602
708
            else:
603
 
                average = convertAverage(average, self.average_scoresystem)
 
709
                average = convertAverage(raw_average, self.average_scoresystem)
604
710
 
605
711
            absences = tardies = 0
606
712
            if (journal_data and not (self.absences_hide and self.tardies_hide)):
607
 
                for meeting in journal_data.recordedMeetings(student):
608
 
                    grade = journal_data.getGrade(student, meeting)
 
713
                # XXX: opt: perm checks may breed here
 
714
                meetings = journal_data.recordedMeetings(student_info['object'])
 
715
                for meeting in meetings:
 
716
                    grade = journal_data.getGrade(
 
717
                        student_info['object'], meeting)
609
718
                    if grade == ABSENT:
610
719
                        absences += 1
611
720
                    elif grade == TARDY:
612
721
                        tardies += 1
613
722
 
614
723
            rows.append(
615
 
                {'student': {'title': student.title,
616
 
                             'id': student.username,
617
 
                             'url': absoluteURL(student, self.request),
618
 
                             'gradeurl': absoluteURL(self.context, self.request) +
619
 
                                    ('/%s' % student.username),
620
 
                            },
 
724
                {'student': student_info,
621
725
                 'grades': grades,
622
726
                 'absences': unicode(absences),
623
727
                 'tardies': unicode(tardies),
624
728
                 'total': total,
625
729
                 'average': average,
 
730
                 'raw_average': raw_average,
626
731
                })
627
732
 
628
733
        # Do the sorting
629
734
        key, reverse = self.sortKey
630
735
        self.collator = ICollator(self.request.locale)
 
736
        def generateStudentKey(row):
 
737
            return self.collator.key(row['student']['title'])
631
738
        def generateKey(row):
632
 
            if key != 'student':
 
739
            if key == 'student':
 
740
                return generateStudentKey(row)
 
741
            elif key == 'total':
 
742
                return (float(row['total']), generateStudentKey(row))
 
743
            elif key == 'average':
 
744
                if row['raw_average'] is UNSCORED:
 
745
                    return ('', generateStudentKey(row))
 
746
                else:
 
747
                    return (row['average'], generateStudentKey(row))
 
748
            elif key in ['absences', 'tardies']:
 
749
                if journal_data is None:
 
750
                    return (0, generateStudentKey(row))
 
751
                else:
 
752
                    return (int(row[key]), generateStudentKey(row))
 
753
            else: # sorting by activity
633
754
                grades = dict([(unicode(grade['activity']), grade['value'])
634
755
                               for grade in row['grades']])
635
756
                if not grades.get(key, ''):
636
 
                    return (1, self.collator.key(row['student']['title']))
 
757
                    return (1, generateStudentKey(row))
637
758
                else:
638
 
                    return (0, grades.get(key), self.collator.key(row['student']['title']))
639
 
            return self.collator.key(row['student']['title'])
 
759
                    return (0, grades.get(key), generateStudentKey(row))
640
760
        return sorted(rows, key=generateKey, reverse=reverse)
641
761
 
642
762
    @property
652
772
            results.append(result)
653
773
        return results
654
774
 
 
775
    @property
 
776
    def deployed(self):
 
777
        gradebook = proxy.removeSecurityProxy(self.context)
 
778
        return gradebook.context.deployed
 
779
 
 
780
 
 
781
class FlourishGradebookOverview(GradebookOverview,
 
782
                                flourish.page.WideContainerPage):
 
783
    """flourish Gradebook Overview/Table"""
 
784
 
 
785
    has_header = False
 
786
    page_class = 'page grid'
 
787
 
 
788
    @property
 
789
    def journal_present(self):
 
790
        section = ISection(proxy.removeSecurityProxy(self.context))
 
791
        return interfaces.ISectionJournalData(section, None) is not None
 
792
 
 
793
    def handleYearChange(self):
 
794
        if 'currentYear' in self.request:
 
795
            currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
796
            currentYear = ISchoolYear(ITerm(currentSection))
 
797
            requestYearId = self.request['currentYear']
 
798
            if requestYearId != currentYear.__name__:
 
799
                for section in self.getUserSections():
 
800
                    year = ISchoolYear(ITerm(section))
 
801
                    if year.__name__ == requestYearId:
 
802
                        newSection = section
 
803
                        break
 
804
                else:
 
805
                    return False
 
806
                url = absoluteURL(newSection, self.request)
 
807
                if self.isTeacher:
 
808
                    url += '/gradebook'
 
809
                else:
 
810
                    url += '/mygrades'
 
811
                self.request.response.redirect(url)
 
812
                return True
 
813
        return False
 
814
 
 
815
    def handlePreferencesChange(self):
 
816
        if not self.isTeacher:
 
817
            return
 
818
        gradebook = proxy.removeSecurityProxy(self.context)
 
819
        columnPreferences = gradebook.getColumnPreferences(self.person)
 
820
        show = self.request.get('show')
 
821
        hide = self.request.get('hide')
 
822
        if show or hide:
 
823
            column_keys_dict = dict(getColumnKeys(gradebook))
 
824
            if show not in column_keys_dict and hide not in column_keys_dict:
 
825
                return
 
826
            if show:
 
827
                prefs = columnPreferences.setdefault(show, {})
 
828
                prefs['hide'] = False
 
829
            if hide:
 
830
                prefs = columnPreferences.setdefault(hide, {})
 
831
                prefs['hide'] = True
 
832
            gradebook.setColumnPreferences(self.person, columnPreferences)
 
833
        elif 'scoresystem' in self.request:
 
834
            vocab = queryUtility(IVocabularyFactory,
 
835
                'schooltool.requirement.discretescoresystems')(None)
 
836
            scoresystem = self.request.get('scoresystem', '')
 
837
            if scoresystem:
 
838
                name = vocab.getTermByToken(scoresystem).value.__name__
 
839
            else:
 
840
                name = scoresystem
 
841
            columnPreferences.setdefault('average', {})['scoresystem'] = name
 
842
            gradebook.setColumnPreferences(self.person, columnPreferences)
 
843
 
 
844
    def handleMoveActivity(self):
 
845
        if not self.isTeacher or self.deployed:
 
846
            return
 
847
        if 'move_left' in self.request:
 
848
            name, change = self.request['move_left'], -1
 
849
        elif 'move_right' in self.request:
 
850
            name, change = self.request['move_right'], 1
 
851
        else:
 
852
            return
 
853
        worksheet = proxy.removeSecurityProxy(self.context).context
 
854
        keys = worksheet.keys()
 
855
        if name in keys:
 
856
            new_pos = keys.index(name) + change
 
857
            if new_pos >= 0 and new_pos < len(keys):
 
858
                worksheet.changePosition(name, new_pos)
 
859
 
 
860
    def handleDeleteActivity(self):
 
861
        if not self.isTeacher or self.deployed:
 
862
            return
 
863
        if 'delete' in self.request:
 
864
            name = self.request['delete']
 
865
            worksheet = proxy.removeSecurityProxy(self.context).context
 
866
            if name in worksheet.keys():
 
867
                del worksheet[name]
 
868
 
 
869
    @property
 
870
    def scoresystems(self):
 
871
        gradebook = proxy.removeSecurityProxy(self.context)
 
872
        columnPreferences = gradebook.getColumnPreferences(self.person)
 
873
        vocab = queryUtility(IVocabularyFactory,
 
874
            'schooltool.requirement.discretescoresystems')(None)
 
875
        current = columnPreferences.get('average', {}).get('scoresystem', '')
 
876
        results = [{
 
877
            'title': _('No score system'),
 
878
            'url': '?scoresystem',
 
879
            'current': not current,
 
880
            }]
 
881
        for term in vocab:
 
882
            results.append({
 
883
                'title': term.value.title,
 
884
                'url': '?scoresystem=%s' % term.token,
 
885
                'current': term.value.__name__ == current,
 
886
                })
 
887
        return results
 
888
 
 
889
    def update(self):
 
890
        """Handle change of current year."""
 
891
        self.person = IPerson(self.request.principal)
 
892
        if self.handleYearChange():
 
893
            return
 
894
 
 
895
        """Handle change of column preferences."""
 
896
        self.handlePreferencesChange()
 
897
 
 
898
        """Handle change of column order."""
 
899
        self.handleMoveActivity()
 
900
 
 
901
        """Handle removal of column."""
 
902
        self.handleDeleteActivity()
 
903
 
 
904
        """Everything else handled by old skin method."""
 
905
        GradebookOverview.update(self)
 
906
 
 
907
 
 
908
class FlourishGradebookYearNavigation(flourish.page.RefineLinksViewlet):
 
909
    """flourish Gradebook Overview year navigation viewlet."""
 
910
 
 
911
 
 
912
class FlourishGradebookYearNavigationViewlet(flourish.viewlet.Viewlet,
 
913
                                             GradebookOverview):
 
914
    template = InlineViewPageTemplate('''
 
915
    <form method="post"
 
916
          tal:attributes="action string:${context/@@absolute_url}">
 
917
      <select name="currentYear" class="navigator"
 
918
              onchange="this.form.submit()">
 
919
        <tal:block repeat="year view/getYears">
 
920
          <option
 
921
              tal:attributes="value year/form_id;
 
922
                              selected year/selected"
 
923
              tal:content="year/title" />
 
924
        </tal:block>
 
925
      </select>
 
926
    </form>
 
927
    ''')
 
928
 
 
929
    @property
 
930
    def person(self):
 
931
        return IPerson(self.request.principal)
 
932
 
 
933
    def getYears(self):
 
934
        currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
935
        currentYear = ISchoolYear(ITerm(currentSection))
 
936
        years = []
 
937
        for section in self.getUserSections():
 
938
            year = ISchoolYear(ITerm(section))
 
939
            if year not in years:
 
940
                years.append(year)
 
941
        return [{'title': year.title,
 
942
                 'form_id': year.__name__,
 
943
                 'selected': year is currentYear and 'selected' or None}
 
944
                for year in years]
 
945
 
 
946
    def render(self, *args, **kw):
 
947
        return self.template(*args, **kw)
 
948
 
 
949
 
 
950
class FlourishGradebookTermNavigation(flourish.page.RefineLinksViewlet):
 
951
    """flourish Gradebook Overview term navigation viewlet."""
 
952
 
 
953
 
 
954
class FlourishGradebookTermNavigationViewlet(flourish.viewlet.Viewlet,
 
955
                                             GradebookOverview):
 
956
    template = InlineViewPageTemplate('''
 
957
    <form method="post"
 
958
          tal:attributes="action string:${context/@@absolute_url}">
 
959
      <select name="currentTerm" class="navigator"
 
960
              onchange="this.form.submit()">
 
961
        <tal:block repeat="term view/getTerms">
 
962
          <option
 
963
              tal:attributes="value term/form_id;
 
964
                              selected term/selected"
 
965
              tal:content="term/title" />
 
966
        </tal:block>
 
967
      </select>
 
968
    </form>
 
969
    ''')
 
970
 
 
971
    @property
 
972
    def person(self):
 
973
        return IPerson(self.request.principal)
 
974
 
 
975
    def getTerms(self):
 
976
        currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
977
        currentTerm = ITerm(currentSection)
 
978
        currentYear = ISchoolYear(currentTerm)
 
979
        terms = []
 
980
        for section in self.getUserSections():
 
981
            term = ITerm(section)
 
982
            if term not in terms and ISchoolYear(term) == currentYear:
 
983
                terms.append(term)
 
984
        return [{'title': term.title,
 
985
                 'form_id': self.getTermId(term),
 
986
                 'selected': term is currentTerm and 'selected' or None}
 
987
                for term in terms]
 
988
 
 
989
    def render(self, *args, **kw):
 
990
        return self.template(*args, **kw)
 
991
 
 
992
 
 
993
class FlourishGradebookSectionNavigation(flourish.page.RefineLinksViewlet):
 
994
    """flourish Gradebook Overview section navigation viewlet."""
 
995
 
 
996
 
 
997
class FlourishGradebookSectionNavigationViewlet(flourish.viewlet.Viewlet,
 
998
                                                GradebookOverview):
 
999
    template = InlineViewPageTemplate('''
 
1000
    <form method="post"
 
1001
          tal:attributes="action string:${context/@@absolute_url}">
 
1002
      <select name="currentSection" class="navigator"
 
1003
              onchange="this.form.submit()">
 
1004
        <tal:block repeat="section view/getSections">
 
1005
          <option
 
1006
              tal:attributes="value section/form_id;
 
1007
                              selected section/selected;"
 
1008
              tal:content="section/title" />
 
1009
        </tal:block>
 
1010
      </select>
 
1011
    </form>
 
1012
    ''')
 
1013
 
 
1014
    @property
 
1015
    def person(self):
 
1016
        return IPerson(self.request.principal)
 
1017
 
 
1018
    def getSections(self):
 
1019
        currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
1020
        currentTerm = ITerm(currentSection)
 
1021
        for section in self.getUserSections():
 
1022
            term = ITerm(section)
 
1023
            if term != currentTerm:
 
1024
                continue
 
1025
            url = absoluteURL(section, self.request)
 
1026
            if self.isTeacher:
 
1027
                url += '/gradebook'
 
1028
            else:
 
1029
                url += '/mygrades'
 
1030
            yield {
 
1031
                'obj': section,
 
1032
                'url': url,
 
1033
                'title': section.title,
 
1034
                'form_id': self.getSectionId(section),
 
1035
                'selected': section==currentSection and 'selected' or None,
 
1036
                }
 
1037
 
 
1038
    def render(self, *args, **kw):
 
1039
        return self.template(*args, **kw)
 
1040
 
 
1041
 
 
1042
class FlourishGradebookOverviewLinks(flourish.page.RefineLinksViewlet):
 
1043
    """flourish Gradebook Overview add links viewlet."""
 
1044
 
 
1045
 
 
1046
class ActivityAddLink(flourish.page.LinkViewlet):
 
1047
 
 
1048
    @property
 
1049
    def title(self):
 
1050
        worksheet = proxy.removeSecurityProxy(self.context).context
 
1051
        if worksheet.deployed:
 
1052
            return ''
 
1053
        return _("Activity")
 
1054
 
 
1055
 
 
1056
class FlourishGradebookSettingsLinks(flourish.page.RefineLinksViewlet):
 
1057
    """flourish Gradebook Settings links viewlet."""
 
1058
 
 
1059
 
 
1060
class GradebookTertiaryNavigationManager(flourish.page.TertiaryNavigationManager):
 
1061
 
 
1062
    template = InlineViewPageTemplate("""
 
1063
        <ul tal:attributes="class view/list_class">
 
1064
          <li tal:repeat="item view/items"
 
1065
              tal:attributes="class item/class"
 
1066
              tal:content="structure item/viewlet">
 
1067
          </li>
 
1068
        </ul>
 
1069
    """)
 
1070
 
 
1071
    @property
 
1072
    def items(self):
 
1073
        result = []
 
1074
        gradebook = proxy.removeSecurityProxy(self.context)
 
1075
        current = gradebook.context.__name__
 
1076
        for worksheet in gradebook.worksheets:
 
1077
            url = '%s/gradebook' % absoluteURL(worksheet, self.request)
 
1078
            result.append({
 
1079
                'class': worksheet.__name__ == current and 'active' or None,
 
1080
                'viewlet': u'<a href="%s">%s</a>' % (url, worksheet.title[:15]),
 
1081
                })
 
1082
        return result
 
1083
 
655
1084
 
656
1085
class GradeActivity(object):
657
1086
    """Grading a single activity"""
724
1153
                self.request.response.redirect('index.html')
725
1154
 
726
1155
 
 
1156
class FlourishGradeActivity(GradeActivity, flourish.page.Page):
 
1157
    """flourish Grading a single activity"""
 
1158
 
 
1159
 
727
1160
def getScoreSystemDiscreteValues(ss):
728
1161
    if IDiscreteValuesScoreSystem.providedBy(ss):
729
1162
        return (ss.scores[-1][2], ss.scores[0][2])
815
1248
        return self.context.getCurrentWorksheet(self.person)
816
1249
 
817
1250
 
 
1251
class FlourishMyGradesView(MyGradesView, flourish.page.Page):
 
1252
    """Flourish student view of own grades."""
 
1253
 
 
1254
    has_header = False
 
1255
 
 
1256
    def handleYearChange(self):
 
1257
        if 'currentYear' in self.request:
 
1258
            currentSection = ISection(proxy.removeSecurityProxy(self.context))
 
1259
            currentYear = ISchoolYear(ITerm(currentSection))
 
1260
            requestYearId = self.request['currentYear']
 
1261
            if requestYearId != currentYear.__name__:
 
1262
                for section in self.getUserSections():
 
1263
                    year = ISchoolYear(ITerm(section))
 
1264
                    if year.__name__ == requestYearId:
 
1265
                        newSection = section
 
1266
                        break
 
1267
                else:
 
1268
                    return False
 
1269
                url = absoluteURL(newSection, self.request)
 
1270
                if self.isTeacher:
 
1271
                    url += '/gradebook'
 
1272
                else:
 
1273
                    url += '/mygrades'
 
1274
                self.request.response.redirect(url)
 
1275
                return True
 
1276
        return False
 
1277
 
 
1278
    def update(self):
 
1279
        """Handle change of year."""
 
1280
        self.person = IPerson(self.request.principal)
 
1281
        if self.handleYearChange():
 
1282
            return
 
1283
 
 
1284
        """Everything else handled by old skin method."""
 
1285
        MyGradesView.update(self)
 
1286
 
 
1287
 
 
1288
class FlourishMyGradesYearNavigation(flourish.page.RefineLinksViewlet):
 
1289
    """flourish MyGrades Overview year navigation viewlet."""
 
1290
 
 
1291
 
 
1292
class FlourishMyGradesYearNavigationViewlet(
 
1293
    FlourishGradebookYearNavigationViewlet):
 
1294
 
 
1295
    isTeacher = False
 
1296
 
 
1297
 
 
1298
class FlourishMyGradesTermNavigation(flourish.page.RefineLinksViewlet):
 
1299
    """flourish MyGrades Overview term navigation viewlet."""
 
1300
 
 
1301
 
 
1302
class FlourishMyGradesTermNavigationViewlet(
 
1303
    FlourishGradebookTermNavigationViewlet):
 
1304
 
 
1305
    isTeacher = False
 
1306
 
 
1307
 
 
1308
class FlourishMyGradesSectionNavigation(flourish.page.RefineLinksViewlet):
 
1309
    """flourish MyGrades Overview section navigation viewlet."""
 
1310
 
 
1311
 
 
1312
class FlourishMyGradesSectionNavigationViewlet(
 
1313
    FlourishGradebookSectionNavigationViewlet):
 
1314
 
 
1315
    isTeacher = False
 
1316
 
 
1317
 
 
1318
class MyGradesTertiaryNavigationManager(flourish.page.TertiaryNavigationManager):
 
1319
 
 
1320
    template = InlineViewPageTemplate("""
 
1321
        <ul tal:attributes="class view/list_class">
 
1322
          <li tal:repeat="item view/items"
 
1323
              tal:attributes="class item/class"
 
1324
              tal:content="structure item/viewlet">
 
1325
          </li>
 
1326
        </ul>
 
1327
    """)
 
1328
 
 
1329
    @property
 
1330
    def items(self):
 
1331
        result = []
 
1332
        gradebook = proxy.removeSecurityProxy(self.context)
 
1333
        current = gradebook.context.__name__
 
1334
        for worksheet in gradebook.worksheets:
 
1335
            url = '%s/mygrades' % absoluteURL(worksheet, self.request)
 
1336
            result.append({
 
1337
                'class': worksheet.__name__ == current and 'active' or None,
 
1338
                'viewlet': u'<a href="%s">%s</a>' % (url, worksheet.title[:15]),
 
1339
                })
 
1340
        return result
 
1341
 
 
1342
 
818
1343
class LinkedActivityGradesUpdater(object):
819
1344
    """Functionality to update grades from a linked activity"""
820
1345
 
1020
1545
        self.person = IPerson(self.request.principal)
1021
1546
        for index, activity in enumerate(self.getFilteredActivities()):
1022
1547
            if interfaces.ILinkedColumnActivity.providedBy(activity):
1023
 
                obj = getSourceObj(activity.source)
1024
 
                if interfaces.IWorksheet.providedBy(obj):
1025
 
                    bestScore = '100'
1026
 
                else:
1027
 
                    bestScore = obj.scoresystem.getBestScore()
1028
 
                title = '%s (%s)' % (obj.title, bestScore)
1029
 
                newSchemaFld = TextLine(
1030
 
                    title=title,
1031
 
                    readonly = True,
1032
 
                    required=False)
 
1548
                continue
 
1549
            elif interfaces.ILinkedActivity.providedBy(activity):
 
1550
                continue
 
1551
            if ICommentScoreSystem.providedBy(activity.scoresystem):
 
1552
                field_cls = HtmlFragment
 
1553
                title = activity.title
1033
1554
            else:
1034
 
                if ICommentScoreSystem.providedBy(activity.scoresystem):
1035
 
                    field_cls = HtmlFragment
1036
 
                    title = activity.title
1037
 
                else:
1038
 
                    field_cls = TextLine
1039
 
                    bestScore = activity.scoresystem.getBestScore()
1040
 
                    title = "%s (%s)" % (activity.title, bestScore)
1041
 
                newSchemaFld = field_cls(
1042
 
                    title=title,
1043
 
                    description=activity.description,
1044
 
                    constraint=activity.scoresystem.fromUnicode,
1045
 
                    required=False)
 
1555
                field_cls = TextLine
 
1556
                bestScore = activity.scoresystem.getBestScore()
 
1557
                title = "%s (%s)" % (activity.title, bestScore)
 
1558
            newSchemaFld = field_cls(
 
1559
                title=title,
 
1560
                description=activity.description,
 
1561
                constraint=activity.scoresystem.fromUnicode,
 
1562
                required=False)
1046
1563
            newSchemaFld.__name__ = str(activity.__name__)
1047
1564
            newSchemaFld.interface = interfaces.IStudentGradebookForm
1048
1565
            newFormFld = field.Field(newSchemaFld)
1146
1663
        return absoluteURL(self.context.gradebook, self.request)
1147
1664
 
1148
1665
 
 
1666
class FlourishGradeStudent(GradeStudent, flourish.form.Form):
 
1667
    """A flourish view for editing a teacher's gradebook column preferences."""
 
1668
 
 
1669
    template = InheritTemplate(flourish.page.Page.template)
 
1670
    label = None
 
1671
    legend = _('Enter grade details below')
 
1672
 
 
1673
    @property
 
1674
    def subtitle(self):
 
1675
        return self.context.student.title
 
1676
 
 
1677
    @button.buttonAndHandler(_('Submit'), name='apply')
 
1678
    def handleApply(self, action):
 
1679
        super(FlourishGradeStudent, self).handleApply.func(self, action)
 
1680
 
 
1681
    @button.buttonAndHandler(_("Previous"))
 
1682
    def handle_previous_action(self, action):
 
1683
        super(FlourishGradeStudent, self).handle_previous_action.func(self,
 
1684
            action)
 
1685
 
 
1686
    @button.buttonAndHandler(_("Next"))
 
1687
    def handle_next_action(self, action):
 
1688
        super(FlourishGradeStudent, self).handle_next_action.func(self,
 
1689
            action)
 
1690
 
 
1691
    @button.buttonAndHandler(_("Cancel"))
 
1692
    def handle_cancel_action(self, action):
 
1693
        super(FlourishGradeStudent, self).handle_cancel_action.func(self,
 
1694
            action)
 
1695
 
 
1696
 
1149
1697
class StudentGradebookView(object):
1150
1698
    """View a student gradebook."""
1151
1699
 
1197
1745
        return activity.due_date < cutoff
1198
1746
 
1199
1747
 
 
1748
class FlourishStudentGradebookView(flourish.page.Page):
 
1749
    """A flourish view of the student gradebook."""
 
1750
 
 
1751
    @property
 
1752
    def title(self):
 
1753
        return self.context.student.title
 
1754
 
 
1755
    @property
 
1756
    def subtitle(self):
 
1757
        gradebook = proxy.removeSecurityProxy(self.context.gradebook)
 
1758
        return _(u'${section} grades for ${worksheet}',
 
1759
                 mapping={'section': ISection(gradebook).title,
 
1760
                          'worksheet': gradebook.context.title})
 
1761
 
 
1762
    def update(self):
 
1763
        gradebook = proxy.removeSecurityProxy(self.context.gradebook)
 
1764
        worksheet = proxy.removeSecurityProxy(gradebook.context)
 
1765
        student = self.context.student
 
1766
        column_keys_dict = dict(getColumnKeys(gradebook))
 
1767
 
 
1768
        person = IPerson(self.request.principal, None)
 
1769
        if person is None:
 
1770
            columnPreferences = {}
 
1771
        else:
 
1772
            columnPreferences = gradebook.getColumnPreferences(person)
 
1773
 
 
1774
        prefs = columnPreferences.get('average', {})
 
1775
        self.average_hide = prefs.get('hide', False)
 
1776
        self.average_label = prefs.get('label', '')
 
1777
        if len(self.average_label) == 0:
 
1778
            self.average_label = column_keys_dict['average']
 
1779
        scoresystems = IScoreSystemContainer(ISchoolToolApplication(None))
 
1780
        self.average_scoresystem = scoresystems.get(
 
1781
            prefs.get('scoresystem', ''))
 
1782
 
 
1783
        self.table = []
 
1784
        count = 0
 
1785
        for activity in gradebook.getWorksheetActivities(worksheet):
 
1786
            activity = proxy.removeSecurityProxy(activity)
 
1787
            score = gradebook.getScore(student, activity)
 
1788
 
 
1789
            if score:
 
1790
                ss = score.scoreSystem
 
1791
                if ICommentScoreSystem.providedBy(ss):
 
1792
                    grade = {
 
1793
                        'comment': True,
 
1794
                        'paragraphs': buildHTMLParagraphs(score.value),
 
1795
                        }
 
1796
                elif IValuesScoreSystem.providedBy(ss):
 
1797
                    s_min, s_max = getScoreSystemDiscreteValues(ss)
 
1798
                    value = score.value
 
1799
                    if IDiscreteValuesScoreSystem.providedBy(ss):
 
1800
                        value = score.scoreSystem.getNumericalValue(score.value)
 
1801
                        if value is None:
 
1802
                            value = 0
 
1803
                    count += s_max - s_min
 
1804
                    grade = {
 
1805
                        'comment': False,
 
1806
                        'value': '%s / %s' % (value, ss.getBestScore()),
 
1807
                        }
 
1808
 
 
1809
                else:
 
1810
                    grade = {
 
1811
                        'comment': False,
 
1812
                        'value': score.value,
 
1813
                        }
 
1814
 
 
1815
            else:
 
1816
                grade = {
 
1817
                    'comment': False,
 
1818
                    'value': '',
 
1819
                    }
 
1820
 
 
1821
            title = activity.title
 
1822
            if activity.description:
 
1823
                title += ' - %s' % activity.description
 
1824
 
 
1825
            row = {
 
1826
                'activity': title,
 
1827
                'grade': grade,
 
1828
                }
 
1829
            self.table.append(row)
 
1830
 
 
1831
        if count:
 
1832
            total, average = gradebook.getWorksheetTotalAverage(worksheet,
 
1833
                student)
 
1834
            self.average = convertAverage(average, self.average_scoresystem)
 
1835
        else:
 
1836
            self.average = None
 
1837
 
 
1838
 
1200
1839
class GradebookCSVView(BrowserView):
1201
1840
 
1202
1841
    def __call__(self):
1229
1868
                    row = [item.encode('utf-8') for item in row]
1230
1869
                    writer.writerow(row)
1231
1870
 
 
1871
 
 
1872
class SectionGradebookLinkViewlet(flourish.page.LinkViewlet):
 
1873
 
 
1874
    @Lazy
 
1875
    def activities(self):
 
1876
        return interfaces.IActivities(self.context)
 
1877
 
 
1878
    @Lazy
 
1879
    def gradebook(self):
 
1880
        person = IPerson(self.request.principal, None)
 
1881
        if person is None:
 
1882
            return None
 
1883
        activities = self.activities
 
1884
        if flourish.canEdit(activities):
 
1885
            ensureAtLeastOneWorksheet(activities)
 
1886
        if not len(activities):
 
1887
            return None
 
1888
        current_worksheet = activities.getCurrentWorksheet(person)
 
1889
        return interfaces.IGradebook(current_worksheet)
 
1890
 
 
1891
    @property
 
1892
    def url(self):
 
1893
        if self.gradebook is None:
 
1894
            return None
 
1895
        return absoluteURL(self.gradebook, self.request)
 
1896
 
 
1897
    @property
 
1898
    def enabled(self):
 
1899
        if not super(SectionGradebookLinkViewlet, self).enabled:
 
1900
            return False
 
1901
        if self.gradebook is None:
 
1902
            return None
 
1903
        can_view = flourish.canView(self.gradebook)
 
1904
        return can_view