158
158
return self.index(*args, **kw)
161
class NarrativeReportBase(object):
163
styles = None # A dict of paragraph styles, initialized later
164
logo = None # A reportlab flowable to be used as logo
166
def __init__(self, logo_filename):
167
if logo_filename is not None:
168
self.logo = self.buildLogo(logo_filename)
171
def setUpStyles(self):
172
from reportlab.lib import enums
174
self.styles['default'] = ParagraphStyle(
175
name='Default', fontName=pdfcal.SANS,
176
fontSize=12, leading=12)
178
self.styles['bold'] = ParagraphStyle(
179
name='DefaultBold', fontName=pdfcal.SANS_BOLD,
180
fontSize=12, leading=12)
182
self.styles['title'] = ParagraphStyle(
183
name='Title', fontName=pdfcal.SANS_BOLD,
184
fontSize=20, leading=22,
185
alignment=enums.TA_CENTER, spaceAfter=6)
187
self.styles['subtitle'] = ParagraphStyle(
188
name='Subtitle', fontName=pdfcal.SANS_BOLD,
189
fontSize=16, leading=22,
190
alignment=enums.TA_CENTER, spaceAfter=6)
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'),
199
def renderPDF(self, story, report_title):
200
datastream = StringIO()
201
doc = SimpleDocTemplate(datastream, pagesize=pagesizes.A4)
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
211
doc.bottomPadding = 0
213
return datastream.getvalue()
215
def buildNarrativeStory(self, narrative):
217
if self.logo is not None:
218
story.append(self.logo)
219
story.append(_para(_('Narrative Report'), self.styles['title']))
221
story.extend(self.buildStudentInfo(narrative))
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))
228
story.extend(self.buildAssesment(narrative))
230
story.append(HRFlowable(
231
width='90%', color=colors.black,
232
spaceBefore=0.5*units.cm))
236
def buildLogo(self, filename):
237
logo = Image(filename)
239
logo.drawHeight = width * (logo.imageHeight / float(logo.imageWidth))
240
logo.drawWidth = width
243
def getPersonName(self, personId):
244
app = ISchoolToolApplication(None)
245
if personId is None or personId not in app['persons']:
247
person = app['persons'][personId]
248
return u'%s %s' % (person.first_name, person.last_name)
250
def buildStudentInfo(self, narrative):
251
student = narrative.student
252
demographics = IDemographics(student)
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)
262
teachers = list(narrative.section.instructors)
264
teacher_name = u'%s %s' % (
265
teachers[0].first_name, teachers[0].last_name)
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'])])
276
[_para(_('Teacher:'), self.styles['bold']),
277
_para(teacher_name, self.styles['default']),
278
_para('', self.styles['bold']),
279
_para(advisor2, self.styles['default'])])
283
widths = [2.5 * units.cm, '50%', 2.5 * units.cm, '50%']
284
story.append(Table(rows, widths, style=self.table_style))
287
[_para(_('Class:'), self.styles['bold']),
288
_para(class_name, self.styles['default']),
291
widths = [2.5 * units.cm, '100%']
292
story.append(Table(rows, widths, style=self.table_style))
295
def buildAssesment(self, narrative):
297
story.append(_para(_('Overall Assessment'), self.styles['subtitle']))
301
[_para(_('Overall Grade:'), self.styles['bold']),
302
_para(narrative.grade, self.styles['default'])])
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.
310
paragraphs = buildHTMLParagraphs(narrative.assessments)
312
paragraphs.append(u'')
314
title = _('Assessments:')
315
for text in paragraphs:
317
[_para(title, self.styles['bold']),
318
Paragraph(text.encode('utf-8'), self.styles['default'])])
321
paragraphs = buildHTMLParagraphs(narrative.comments)
323
paragraphs.append(u'')
325
title = _('Comments:')
326
for text in paragraphs:
328
[_para(title, self.styles['bold']),
329
Paragraph(text.encode('utf-8'), self.styles['default'])])
332
widths = [3.5 * units.cm, '100%']
333
story.append(Table(rows, widths, style=self.table_style))
337
class StudentNarrtiveReport(NarrativeReportBase):
339
narrative = None # The narrative entry
343
student = self.narrative.student
344
return _('Narrative report for %s %s') % (
345
student.first_name, student.last_name)
347
def __init__(self, logo_filename, narrative):
348
super(StudentNarrtiveReport, self).__init__(logo_filename)
349
self.narrative = narrative
352
"""Build and render the report"""
353
story = self.buildNarrativeStory(self.narrative)
354
pdf_data = self.renderPDF(story, self.title)
358
class AggregateNarrativeReport(NarrativeReportBase):
360
title = _('Aggregate narrative report')
361
narrative = None # The narrative root
363
def __init__(self, logo_filename, narratives):
364
super(AggregateNarrativeReport, self).__init__(logo_filename)
365
self.narratives = narratives
367
def buildAggregateStory(self, narratives):
369
for narrative in narratives:
370
story.extend(self.buildNarrativeStory(narrative))
371
story.append(PageBreak())
375
"""Build and render the report"""
376
story = self.buildAggregateStory(self.narratives)
377
pdf_data = self.renderPDF(story, self.title)
381
class NarrativePDFView(BrowserView):
382
"""The narrative report card (PDF)"""
384
pdf_disabled_text = _("PDF support is disabled."
385
" It can be enabled by your administrator.")
388
def pdf_support_disabled(self):
389
return pdfcal.disabled
392
"""Return the PDF representation of a calendar."""
393
if self.pdf_support_disabled:
394
return translate(self.pdf_disabled_text, context=self.request)
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')
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:
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
421
tmp_logo = file(tempfile.mktemp(suffix='.png'), 'w+b')
422
tmp_logo.write(logo_resource.GET())
429
logo_filename = self.createTempLogo()
430
report = StudentNarrtiveReport(
431
logo_filename, removeSecurityProxy(self.context))
435
class AggregateNarrativePDFView(NarrativePDFView):
436
"""Aggregated narrative reports of all students."""
438
no_narratives_template = ViewPageTemplateFile(
439
'no_narratives_for_report.pt')
441
def __init__(self, *args, **kw):
442
super(AggregateNarrativePDFView, self).__init__(*args, **kw)
443
self.narratives = self.collectNarratives()
446
if not self.narratives:
447
return self.no_narratives_template()
448
return super(AggregateNarrativePDFView, self).__call__()
451
"""Empty method needed for the page template"""
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)
461
def _getStudentSortKey(self, student):
462
"""Sort key is a list of person titles:
464
[primary advisor, secondary advisor, student]
466
demographics = getDemographicsByUsername(student.__name__)
468
if demographics.advisor1 is not None:
469
return [self._getPersonTitle(demographics.advisor1),
470
self._getPersonTitle(demographics.advisor2),
471
self._getPersonTitle(student.__name__)]
473
return [self._getPersonTitle(demographics.advisor2),
474
self._getPersonTitle(demographics.advisor1),
475
self._getPersonTitle(student.__name__)]
477
def collectNarratives(self):
479
schoolyear = getInterventionSchoolYear()
480
for student in sorted(schoolyear.values(),
481
key=self._getStudentSortKey):
482
narratives.extend(list(student['narratives'].values()))
486
logo_filename = self.createTempLogo()
487
report = AggregateNarrativeReport(logo_filename, self.narratives)
491
class StudentNarrativesPDFView(AggregateNarrativePDFView):
492
"""Aggregated narrative reports of a student."""
494
def collectNarratives(self):
495
return list(self.context['narratives'].values())
498
def getSectionStudentIds(section):
499
"""Retrieve the section member ids"""
501
for member in section.members:
502
if IPerson.providedBy(member):
503
stud_ids.add(member.username)
504
elif IGroup.providedBy(member):
507
for person in filter(IPerson.providedBy, member.members)])
511
class SectionNarrativesPDFView(AggregateNarrativePDFView):
512
"""Aggregated narrative reports of a section."""
514
def _getStudentSortKey(self, student):
515
return self._getPersonTitle(student.__name__)
517
def collectNarratives(self):
518
section = self.context
519
section_id = section.__name__
520
schoolyear = getInterventionSchoolYear()
522
students = sorted([schoolyear[stud_id]
523
for stud_id in getSectionStudentIds(section)
524
if stud_id in schoolyear],
525
key=self._getStudentSortKey)
527
narratives = [student['narratives'][section_id]
528
for student in students
529
if section_id in student['narratives']]
534
class TaughtNarrativesPDFView(AggregateNarrativePDFView):
535
"""Aggregated narrative reports of a teachers's students."""
537
def _getStudentSortKey(self, student):
538
return self._getPersonTitle(student.__name__)
540
def collectNarratives(self):
541
teacher = self.context
542
sections = getRelatedObjects(teacher, URISection,
543
rel_type=URIInstruction)
547
section_ids = [section.__name__ for section in sections]
550
for section in sections:
551
stud_ids.update(getSectionStudentIds(section))
553
schoolyear = getInterventionSchoolYear()
555
students = sorted([schoolyear[stud_id]
556
for stud_id in stud_ids
557
if stud_id in schoolyear],
558
key=self._getStudentSortKey)
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])
569
class AdvisedNarrativesPDFView(AggregateNarrativePDFView):
570
"""Aggregated narrative reports of advised students."""
572
def _getStudentSortKey(self, student):
573
return self._getPersonTitle(student.__name__)
575
def collectNarratives(self):
576
advisor = self.context
577
advisor_id = advisor.__name__
579
schoolyear = getInterventionSchoolYear()
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)
589
for student in sorted(students, key=self._getStudentSortKey):
590
narratives.extend(list(student['narratives'].values()))
595
class GradebookCSVView(BrowserView):
597
datastream = StringIO()
598
app = ISchoolToolApplication(None)
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),
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)
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:
621
sectionValues.append(value)
622
values = ['"%s"' % value
623
for value in studentValues + sectionValues]
624
datastream.write('%s\n' % ', '.join(values))
626
return datastream.getvalue()
628
def getSections(self, student):
629
for item in student.groups:
630
if ISection.providedBy(item):