~aelkner/schooltool/schooltool.sla_august_fixes

« back to all changes in this revision

Viewing changes to src/schooltool/intervention/browser/reports.py

  • Committer: Justas Sadzevicius
  • Date: 2009-08-14 09:48:48 UTC
  • mfrom: (163.1.19 sla)
  • Revision ID: justas@pov.lt-20090814094848-ajt79v222ovkv9tj
Merge sla intervetion migration to the gradebook package.

Show diffs side-by-side

added added

removed removed

Lines of Context:
54
54
from schooltool.app.interfaces import ISchoolToolApplication
55
55
from schooltool.app.browser import pdfcal
56
56
from schooltool.common import SchoolToolMessage as _
57
 
from schooltool.course.interfaces import ISection
58
 
from schooltool.gradebook.interfaces import IActivities
59
 
from schooltool.intervention.interfaces import IDemographics
60
 
from schooltool.intervention.intervention import getInterventionSchoolYear
61
 
from schooltool.intervention.demographics import getDemographicsByUsername
62
 
from schooltool.requirement.interfaces import IEvaluations
63
 
from schooltool.requirement.scoresystem import UNSCORED
64
57
 
65
58
 
66
59
ReportsCSSViewlet = CSSViewlet("reportsViewlet.css")
84
77
    text = text.replace(u'>', u'>')
85
78
    text = text.replace(u'"', u'"')
86
79
    text = text.replace(u''', u"'")
 
80
    text = text.replace(u'’', u"'")
87
81
    text = text.replace(u' ', u' ')
88
82
    return text
89
83
 
91
85
escaped_reportlab_tags_re = re.compile(
92
86
    r'<(/?((strong)|(b)|(em)|(i)))>')
93
87
 
94
 
html_p_tag_re = re.compile(r'</?p>')
 
88
html_p_tag_re = re.compile(r'</?p[^>]*>')
 
89
html_br_tag_re = re.compile(r'</?br[^>]*>')
95
90
 
96
91
 
97
92
def buildHTMLParagraphs(snippet):
99
94
    if not snippet:
100
95
        return []
101
96
    paragraphs = []
 
97
    tokens = []
102
98
    for token in html_p_tag_re.split(snippet):
103
99
        if not token or token.isspace():
104
100
            continue
 
101
        tokens.extend(html_br_tag_re.split(token))
 
102
    for token in tokens:
 
103
        if not token or token.isspace():
 
104
            continue
105
105
        # Reportlab is very sensitive to unknown tags and escaped symbols.
106
106
        # In case of invalid HTML, ensure correct escaping.
107
107
        fixed_escaping = cgi.escape(_unescape_FCKEditor_HTML(unicode(token)))
157
157
            return ''
158
158
        return self.index(*args, **kw)
159
159
 
160
 
 
161
 
class NarrativeReportBase(object):
162
 
 
163
 
    styles = None # A dict of paragraph styles, initialized later
164
 
    logo = None # A reportlab flowable to be used as logo
165
 
 
166
 
    def __init__(self, logo_filename):
167
 
        if logo_filename is not None:
168
 
            self.logo = self.buildLogo(logo_filename)
169
 
        self.setUpStyles()
170
 
 
171
 
    def setUpStyles(self):
172
 
        from reportlab.lib import enums
173
 
        self.styles = {}
174
 
        self.styles['default'] = ParagraphStyle(
175
 
            name='Default', fontName=pdfcal.SANS,
176
 
            fontSize=12, leading=12)
177
 
 
178
 
        self.styles['bold'] = ParagraphStyle(
179
 
            name='DefaultBold', fontName=pdfcal.SANS_BOLD,
180
 
            fontSize=12, leading=12)
181
 
 
182
 
        self.styles['title'] = ParagraphStyle(
183
 
            name='Title', fontName=pdfcal.SANS_BOLD,
184
 
            fontSize=20, leading=22,
185
 
            alignment=enums.TA_CENTER, spaceAfter=6)
186
 
 
187
 
        self.styles['subtitle'] = ParagraphStyle(
188
 
            name='Subtitle', fontName=pdfcal.SANS_BOLD,
189
 
            fontSize=16, leading=22,
190
 
            alignment=enums.TA_CENTER, spaceAfter=6)
191
 
 
192
 
        self.table_style = TableStyle(
193
 
          [('LEFTPADDING', (0, 0), (-1, -1), 1),
194
 
           ('RIGHTPADDING', (0, 0), (-1, -1), 1),
195
 
           ('ALIGN', (1, 0), (-1, -1), 'LEFT'),
196
 
           ('VALIGN', (0, 0), (-1, -1), 'TOP'),
197
 
          ])
198
 
 
199
 
    def renderPDF(self, story, report_title):
200
 
        datastream = StringIO()
201
 
        doc = SimpleDocTemplate(datastream, pagesize=pagesizes.A4)
202
 
        title = report_title
203
 
        doc.title = title.encode('utf-8')
204
 
        doc.leftMargin = 0.75 * units.inch
205
 
        doc.bottomMargin = 0.75 * units.inch
206
 
        doc.topMargin = 0.75 * units.inch
207
 
        doc.rightMargin = 0.75 * units.inch
208
 
        doc.leftPadding = 0
209
 
        doc.rightPadding = 0
210
 
        doc.topPadding = 0
211
 
        doc.bottomPadding = 0
212
 
        doc.build(story)
213
 
        return datastream.getvalue()
214
 
 
215
 
    def buildNarrativeStory(self, narrative):
216
 
        story = []
217
 
        if self.logo is not None:
218
 
            story.append(self.logo)
219
 
        story.append(_para(_('Narrative Report'), self.styles['title']))
220
 
 
221
 
        story.extend(self.buildStudentInfo(narrative))
222
 
 
223
 
        # append horizontal rule
224
 
        story.append(HRFlowable(
225
 
            width='90%', color=colors.black,
226
 
            spaceBefore=0.5*units.cm, spaceAfter=0.5*units.cm))
227
 
 
228
 
        story.extend(self.buildAssesment(narrative))
229
 
 
230
 
        story.append(HRFlowable(
231
 
            width='90%', color=colors.black,
232
 
            spaceBefore=0.5*units.cm))
233
 
 
234
 
        return story
235
 
 
236
 
    def buildLogo(self, filename):
237
 
        logo = Image(filename)
238
 
        width = 8 * units.cm
239
 
        logo.drawHeight = width * (logo.imageHeight / float(logo.imageWidth))
240
 
        logo.drawWidth = width
241
 
        return logo
242
 
 
243
 
    def getPersonName(self, personId):
244
 
        app = ISchoolToolApplication(None)
245
 
        if personId is None or personId not in app['persons']:
246
 
            return ''
247
 
        person = app['persons'][personId]
248
 
        return u'%s %s' % (person.first_name, person.last_name)
249
 
 
250
 
    def buildStudentInfo(self, narrative):
251
 
        student = narrative.student
252
 
        demographics = IDemographics(student)
253
 
 
254
 
        student_name = u'%s %s' % (
255
 
            narrative.student.first_name, narrative.student.last_name)
256
 
        class_name = u'%s - %s' % (
257
 
            list(narrative.section.courses)[0].title,
258
 
            narrative.section.title)
259
 
        advisor1 = self.getPersonName(demographics.advisor1)
260
 
        advisor2 = self.getPersonName(demographics.advisor2)
261
 
 
262
 
        teachers = list(narrative.section.instructors)
263
 
        if teachers:
264
 
            teacher_name = u'%s %s' % (
265
 
                teachers[0].first_name, teachers[0].last_name)
266
 
        else:
267
 
            teacher_name = u''
268
 
 
269
 
        rows = []
270
 
        rows.append(
271
 
            [_para(_('Student:'), self.styles['bold']),
272
 
             _para(student_name, self.styles['default']),
273
 
             _para(_('Advisor(s):'), self.styles['bold']),
274
 
             _para(advisor1, self.styles['default'])])
275
 
        rows.append(
276
 
            [_para(_('Teacher:'), self.styles['bold']),
277
 
             _para(teacher_name, self.styles['default']),
278
 
             _para('', self.styles['bold']),
279
 
             _para(advisor2, self.styles['default'])])
280
 
 
281
 
        story = []
282
 
 
283
 
        widths = [2.5 * units.cm, '50%', 2.5 * units.cm, '50%']
284
 
        story.append(Table(rows, widths, style=self.table_style))
285
 
 
286
 
        rows = [
287
 
            [_para(_('Class:'), self.styles['bold']),
288
 
             _para(class_name, self.styles['default']),
289
 
             ]]
290
 
 
291
 
        widths = [2.5 * units.cm, '100%']
292
 
        story.append(Table(rows, widths, style=self.table_style))
293
 
        return story
294
 
 
295
 
    def buildAssesment(self, narrative):
296
 
        story = []
297
 
        story.append(_para(_('Overall Assessment'), self.styles['subtitle']))
298
 
 
299
 
        rows = []
300
 
        rows.append(
301
 
            [_para(_('Overall Grade:'), self.styles['bold']),
302
 
             _para(narrative.grade, self.styles['default'])])
303
 
 
304
 
        # We will now do some manual text paragraph splitting and put
305
 
        # them into separate cells.
306
 
        # For one - reportlab Paragraph flowables do not handle newlines.
307
 
        # Another reason - there is a bug in reportlab Table rendering and
308
 
        # it can't handle cells bigger than a page.
309
 
 
310
 
        paragraphs = buildHTMLParagraphs(narrative.assessments)
311
 
        if not paragraphs:
312
 
            paragraphs.append(u'')
313
 
 
314
 
        title = _('Assessments:')
315
 
        for text in paragraphs:
316
 
            rows.append(
317
 
                [_para(title, self.styles['bold']),
318
 
                 Paragraph(text.encode('utf-8'), self.styles['default'])])
319
 
            title = ''
320
 
 
321
 
        paragraphs = buildHTMLParagraphs(narrative.comments)
322
 
        if not paragraphs:
323
 
            paragraphs.append(u'')
324
 
 
325
 
        title = _('Comments:')
326
 
        for text in paragraphs:
327
 
            rows.append(
328
 
                [_para(title, self.styles['bold']),
329
 
                 Paragraph(text.encode('utf-8'), self.styles['default'])])
330
 
            title = ''
331
 
 
332
 
        widths = [3.5 * units.cm, '100%']
333
 
        story.append(Table(rows, widths, style=self.table_style))
334
 
        return story
335
 
 
336
 
 
337
 
class StudentNarrtiveReport(NarrativeReportBase):
338
 
 
339
 
    narrative = None # The narrative entry
340
 
 
341
 
    @property
342
 
    def title(self):
343
 
        student = self.narrative.student
344
 
        return _('Narrative report for %s %s') % (
345
 
            student.first_name, student.last_name)
346
 
 
347
 
    def __init__(self, logo_filename, narrative):
348
 
        super(StudentNarrtiveReport, self).__init__(logo_filename)
349
 
        self.narrative = narrative
350
 
 
351
 
    def __call__(self):
352
 
        """Build and render the report"""
353
 
        story = self.buildNarrativeStory(self.narrative)
354
 
        pdf_data = self.renderPDF(story, self.title)
355
 
        return pdf_data
356
 
 
357
 
 
358
 
class AggregateNarrativeReport(NarrativeReportBase):
359
 
 
360
 
    title = _('Aggregate narrative report')
361
 
    narrative = None # The narrative root
362
 
 
363
 
    def __init__(self, logo_filename, narratives):
364
 
        super(AggregateNarrativeReport, self).__init__(logo_filename)
365
 
        self.narratives = narratives
366
 
 
367
 
    def buildAggregateStory(self, narratives):
368
 
        story = []
369
 
        for narrative in narratives:
370
 
            story.extend(self.buildNarrativeStory(narrative))
371
 
            story.append(PageBreak())
372
 
        return story
373
 
 
374
 
    def __call__(self):
375
 
        """Build and render the report"""
376
 
        story = self.buildAggregateStory(self.narratives)
377
 
        pdf_data = self.renderPDF(story, self.title)
378
 
        return pdf_data
379
 
 
380
 
 
381
 
class NarrativePDFView(BrowserView):
382
 
    """The narrative report card (PDF)"""
383
 
 
384
 
    pdf_disabled_text = _("PDF support is disabled."
385
 
                          "  It can be enabled by your administrator.")
386
 
 
387
 
    @property
388
 
    def pdf_support_disabled(self):
389
 
        return pdfcal.disabled
390
 
 
391
 
    def __call__(self):
392
 
        """Return the PDF representation of a calendar."""
393
 
        if self.pdf_support_disabled:
394
 
            return translate(self.pdf_disabled_text, context=self.request)
395
 
 
396
 
        pdf_data = self.buildPDF()
397
 
        response = self.request.response
398
 
        response.setHeader('Content-Type', 'application/pdf')
399
 
        response.setHeader('Content-Length', len(pdf_data))
400
 
        response.setHeader("pragma", "no-store,no-cache")
401
 
        response.setHeader("cache-control",
402
 
                           "no-cache, no-store,must-revalidate, max-age=-1")
403
 
        response.setHeader("expires", "-1")
404
 
        # We don't really accept ranges, but Acrobat Reader will not show the
405
 
        # report in the browser page if this header is not provided.
406
 
        response.setHeader('Accept-Ranges', 'bytes')
407
 
        return pdf_data
408
 
 
409
 
    def createTempLogo(self):
410
 
        """Create a temporary logo file and return the filename"""
411
 
        logo_resource = queryAdapter(
412
 
            self.request, name='sla_report_logo.png',
413
 
            context=self.context)
414
 
        if logo_resource is None:
415
 
            return None
416
 
        # We will now create a temporay file containing the image,
417
 
        # as reportlab can create Image flowables only from given filename.
418
 
        # Fixing this seemed too expensive when the narrative pdf reports
419
 
        # were implemented.
420
 
        try:
421
 
            tmp_logo = file(tempfile.mktemp(suffix='.png'), 'w+b')
422
 
            tmp_logo.write(logo_resource.GET())
423
 
            tmp_logo.close()
424
 
        except IOError:
425
 
            return None
426
 
        return tmp_logo.name
427
 
 
428
 
    def buildPDF(self):
429
 
        logo_filename = self.createTempLogo()
430
 
        report = StudentNarrtiveReport(
431
 
            logo_filename, removeSecurityProxy(self.context))
432
 
        return report()
433
 
 
434
 
 
435
 
class AggregateNarrativePDFView(NarrativePDFView):
436
 
    """Aggregated narrative reports of all students."""
437
 
 
438
 
    no_narratives_template = ViewPageTemplateFile(
439
 
        'no_narratives_for_report.pt')
440
 
 
441
 
    def __init__(self, *args, **kw):
442
 
        super(AggregateNarrativePDFView, self).__init__(*args, **kw)
443
 
        self.narratives = self.collectNarratives()
444
 
 
445
 
    def __call__(self):
446
 
        if not self.narratives:
447
 
            return self.no_narratives_template()
448
 
        return super(AggregateNarrativePDFView, self).__call__()
449
 
 
450
 
    def update(self):
451
 
        """Empty method needed for the page template"""
452
 
 
453
 
    def _getPersonTitle(self, person_id):
454
 
        """Obtain person sorting title from person id"""
455
 
        app = ISchoolToolApplication(None)
456
 
        person = app['persons'].get(person_id)
457
 
        if person is None:
458
 
            return u''
459
 
        return person.title
460
 
 
461
 
    def _getStudentSortKey(self, student):
462
 
        """Sort key is a list of person titles:
463
 
 
464
 
        [primary advisor, secondary advisor, student]
465
 
        """
466
 
        demographics = getDemographicsByUsername(student.__name__)
467
 
 
468
 
        if demographics.advisor1 is not None:
469
 
            return [self._getPersonTitle(demographics.advisor1),
470
 
                    self._getPersonTitle(demographics.advisor2),
471
 
                    self._getPersonTitle(student.__name__)]
472
 
        else:
473
 
            return [self._getPersonTitle(demographics.advisor2),
474
 
                    self._getPersonTitle(demographics.advisor1),
475
 
                    self._getPersonTitle(student.__name__)]
476
 
 
477
 
    def collectNarratives(self):
478
 
        narratives = []
479
 
        schoolyear = getInterventionSchoolYear()
480
 
        for student in sorted(schoolyear.values(),
481
 
                              key=self._getStudentSortKey):
482
 
            narratives.extend(list(student['narratives'].values()))
483
 
        return narratives
484
 
 
485
 
    def buildPDF(self):
486
 
        logo_filename = self.createTempLogo()
487
 
        report = AggregateNarrativeReport(logo_filename, self.narratives)
488
 
        return report()
489
 
 
490
 
 
491
 
class StudentNarrativesPDFView(AggregateNarrativePDFView):
492
 
    """Aggregated narrative reports of a student."""
493
 
 
494
 
    def collectNarratives(self):
495
 
        return list(self.context['narratives'].values())
496
 
 
497
 
 
498
 
def getSectionStudentIds(section):
499
 
    """Retrieve the section member ids"""
500
 
    stud_ids = set()
501
 
    for member in section.members:
502
 
        if IPerson.providedBy(member):
503
 
            stud_ids.add(member.username)
504
 
        elif IGroup.providedBy(member):
505
 
            stud_ids.update([
506
 
                person.username
507
 
                for person in filter(IPerson.providedBy, member.members)])
508
 
    return stud_ids
509
 
 
510
 
 
511
 
class SectionNarrativesPDFView(AggregateNarrativePDFView):
512
 
    """Aggregated narrative reports of a section."""
513
 
 
514
 
    def _getStudentSortKey(self, student):
515
 
        return self._getPersonTitle(student.__name__)
516
 
 
517
 
    def collectNarratives(self):
518
 
        section = self.context
519
 
        section_id = section.__name__
520
 
        schoolyear = getInterventionSchoolYear()
521
 
 
522
 
        students = sorted([schoolyear[stud_id]
523
 
                           for stud_id in getSectionStudentIds(section)
524
 
                           if stud_id in schoolyear],
525
 
                          key=self._getStudentSortKey)
526
 
 
527
 
        narratives = [student['narratives'][section_id]
528
 
                      for student in students
529
 
                      if section_id in student['narratives']]
530
 
 
531
 
        return narratives
532
 
 
533
 
 
534
 
class TaughtNarrativesPDFView(AggregateNarrativePDFView):
535
 
    """Aggregated narrative reports of a teachers's students."""
536
 
 
537
 
    def _getStudentSortKey(self, student):
538
 
        return self._getPersonTitle(student.__name__)
539
 
 
540
 
    def collectNarratives(self):
541
 
        teacher = self.context
542
 
        sections = getRelatedObjects(teacher, URISection,
543
 
                                     rel_type=URIInstruction)
544
 
        if not sections:
545
 
            return []
546
 
 
547
 
        section_ids = [section.__name__ for section in sections]
548
 
 
549
 
        stud_ids = set()
550
 
        for section in sections:
551
 
            stud_ids.update(getSectionStudentIds(section))
552
 
 
553
 
        schoolyear = getInterventionSchoolYear()
554
 
 
555
 
        students = sorted([schoolyear[stud_id]
556
 
                           for stud_id in stud_ids
557
 
                           if stud_id in schoolyear],
558
 
                          key=self._getStudentSortKey)
559
 
 
560
 
        narratives = []
561
 
        for section_id in section_ids:
562
 
            for student in students:
563
 
                if section_id in student['narratives']:
564
 
                    narratives.append(student['narratives'][section_id])
565
 
 
566
 
        return narratives
567
 
 
568
 
 
569
 
class AdvisedNarrativesPDFView(AggregateNarrativePDFView):
570
 
    """Aggregated narrative reports of advised students."""
571
 
 
572
 
    def _getStudentSortKey(self, student):
573
 
        return self._getPersonTitle(student.__name__)
574
 
 
575
 
    def collectNarratives(self):
576
 
        advisor = self.context
577
 
        advisor_id = advisor.__name__
578
 
 
579
 
        schoolyear = getInterventionSchoolYear()
580
 
 
581
 
        students = []
582
 
        for student in schoolyear.values():
583
 
            demographics = getDemographicsByUsername(student.__name__)
584
 
            if (demographics.advisor1 == advisor_id or
585
 
                demographics.advisor2 == advisor_id):
586
 
                students.append(student)
587
 
 
588
 
        narratives = []
589
 
        for student in sorted(students, key=self._getStudentSortKey):
590
 
            narratives.extend(list(student['narratives'].values()))
591
 
 
592
 
        return narratives
593
 
 
594
 
 
595
 
class GradebookCSVView(BrowserView):
596
 
    def __call__(self):
597
 
        datastream = StringIO()
598
 
        app = ISchoolToolApplication(None)
599
 
 
600
 
        for id, student in sorted(app['persons'].items()):
601
 
            demos = getDemographicsByUsername(id)
602
 
            evaluations = IEvaluations(student)
603
 
            studentValues = ['%s %s' % (student.first_name, student.last_name), 
604
 
                demos.district_id]
605
 
 
606
 
            for section in self.getSections(student):
607
 
                teacher = list(section.instructors)[0]
608
 
                course = list(section.courses)[0]
609
 
                sectionValues = ['%s %s' % (teacher.first_name, 
610
 
                    teacher.last_name), course.title, section.title]
611
 
                worksheets = IActivities(section)
612
 
 
613
 
                if 'quarter1' in worksheets:
614
 
                    quarter1 = worksheets['quarter1']
615
 
                    for activity in quarter1.values():
616
 
                        ev = evaluations.get(activity, None)
617
 
                        if ev is not None and ev.value is not UNSCORED:
618
 
                            value = ev.value
619
 
                        else:
620
 
                            value = ''
621
 
                        sectionValues.append(value)
622
 
                    values = ['"%s"' % value 
623
 
                              for value in studentValues + sectionValues]
624
 
                    datastream.write('%s\n' % ', '.join(values))
625
 
 
626
 
        return datastream.getvalue()
627
 
 
628
 
    def getSections(self, student):
629
 
        for item in student.groups:
630
 
            if ISection.providedBy(item):
631
 
                yield item
632