# This file is part of ReportTool
# ReportTool (Felicity) is copyright 2004-8 Steve Butterfill.
#
# ReportTool is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# ReportTool is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ReportTool. If not, see .
#
# If you want to use ReportTool under a difference licence, email
# s.butterfill@warwick.ac.uk.
# set of methods for converting reports to csv and pdf
# (c) 2007 Steve Butterfill
# use subject to licence. See licence.txt
import csv
from StringIO import StringIO
from datetime import datetime
#for pdf
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
PageBreak, Table, TableStyle, )
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch
from felicity.hacks import safe_str
def parse_content(content, format, show_item_numbers=False):
"""modifies the content to a nice form by putting in values for any options from the format.
Otherwise keeps the list of items exactly as it is."""
counter = 0
new_content = []
for item in content:
field = format.fields[counter]
if item is None:
#avoid possible problems from None items
new_content.append(item)
elif field.field_type=="select-one":
try:
item_name = field.options[int(item)]
except (IndexError, ValueError), e:
item_name = "unknown (invalid data)"
if show_item_numbers:
new_content.append(str(item)+":"+item_name)
else:
new_content.append(item_name)
elif field.field_type=='select-many':
#in this case item is a list of selected options
new_item = []
for selected_option in item:
try:
item_name = field.options[int(selected_option)]
except IndexError:
item_name = "unknown (invalid data)"
if show_item_numbers:
new_item.append(str(selected_option)+":"+item_name)
else:
new_item.append(item_name)
new_content.append(new_item)
else:
new_content.append(item)
counter+=1
return new_content
def _report_content_to_pairs(report):
"""converts the content of a report to a list of pairs.
The first pair is the name of the field. The second pair is the value of the field.
Where the field is a select option, the selection name is filled in."""
rf=report.format
field_names = [x.name for x in rf.fields]
content=parse_content(report.content,rf)
return zip(field_names,content)
def _student_comments_to_pairs(report):
"""converts the student_comments to pairs.
See _report_content_to_pairs"""
student_comments = report.student_comments
make_field_name = lambda sc: "%s, %s:" % (sc.name, sc.created_formatted)
make_text = lambda sc: sc.text.replace("\r","")
pairs = [(make_field_name(sc), make_text(sc)) for sc in student_comments]
return pairs
def _getattr_nest(object, attr_name):
"""getattr for 'prop.innerprop'"""
attr_names = [object] + attr_name.split('.')
return reduce(getattr, attr_names)
def _list_header_names(headers):
"""headers is a list of pairs, the first of which is the names"""
result = [pair[0] for pair in headers]
return result
def _list_header_values(headers, report):
"""headers is a list of pairs, the second of which is at attr.
This is called on the report to get the value"""
result = []
for pair in headers:
result.append(_getattr_nest(report,pair[1]))
return result
def _list_header_pairs(headers, report):
"""headers is a list of pairs, the second of which is at attr.
This is called on the report to get the value.
The returned list is a pair of header name with header values"""
result = []
for pair in headers:
result.append( (pair[0],
_getattr_nest(report,pair[1]) ) )
return result
def to_csv(reports):
"""returns a string containing a csv file with the reports in it.
Uses the _list_header_names etc methods which gives flexibility but may be slow.
Also works with list of Feedback objects. Can't cope with a mix of Report
and Feedback objects."""
#first check which reportformats are used
rfs = []
for report in reports:
if report.format not in rfs:
rfs.append(report.format)
lines = []
multi_format = len(rfs)>1
if multi_format:
lines.append(["This file contains reports in more than one format."])
lines.append(["If you scroll down you will see new header lines for every different format."])
lines.append([""])
if reports is not None and len(reports)>0:
basic_header = _list_header_names(reports[0]._standard_headers)
#cycle through each rf and create lines for it
for rf in rfs:
if multi_format:
lines.append([""]) #separator
content_field_names = [x.name for x in rf.fields]
header = basic_header+content_field_names
lines.append(header)
for report in reports:
if multi_format and report.format != rf:
continue #skip any reports not of this format
line = _list_header_values(headers=report._standard_headers, report=report)
line += parse_content(report.content, rf)
#convert line to utf-8 because csv writer doesn't do unicode
line = [safe_str(s).encode("utf-8") for s in line]
lines.append(line)
#convert all the lines to csv
out_file = StringIO()
writer = csv.writer(out_file)
writer.writerows(lines)
return out_file.getvalue()
def _title_para(txt):
return '' + txt + ''
def _bold_para(txt):
return '' + txt + ''
def _normal_para(txt):
return '' + txt + ''
def _indent_para(txt):
return '' + txt + ''
def _pairs_to_paras(pairs, components, paragraphStyle, bold_headers=True):
"""converts a list of pairs to paragraphs. First item in pair is heading,
second item is value. Paragraphs are appended to components"""
for pair in pairs:
if bold_headers:
header = _bold_para(pair[0])
else:
header = _normal_para(pair[0])
components.append(Paragraph(header, paragraphStyle))
components.append(Paragraph(_indent_para(pair[1]), paragraphStyle))
components.append(Spacer(1,0.125*inch))
def to_pdf(reports):
"""converts a list of reports into a pdf file, one report per page.
Only works for Report objects, not for Feedback."""
paragraphStyle = getSampleStyleSheet()['Normal']
components = [] #this will hold a list of paragraphs
#we use a table for the basic headers, this is the style
tableStyle = TableStyle([
('ALIGN',(0,0),(-1,-1),'LEFT'),
('LEFTPADDING',(0,0),(-1,-1),0),
])
#these paragraphs are always included
for report in reports:
#title
term_year = str(report.term) +" "+ str(report.year)
txt = _title_para("Department of Philosophy Report " + term_year)
components.append(Paragraph(txt, paragraphStyle))
#report headers
components.append(Spacer(1,0.5*inch))
basic_pairs = _list_header_pairs(report._pdf_headers, report)
basic_pairs.append(('Date Printed',datetime.now().strftime("%d %b %Y %H:%M")))
tbl = Table(basic_pairs,[1.5*inch,None], style=tableStyle)
tbl.hAlign = "LEFT"
components.append( tbl)
components.append(Spacer(1,0.25*inch))
#report content
report_content_pairs= _report_content_to_pairs(report)
_pairs_to_paras(report_content_pairs,components, paragraphStyle)
#student comments
if len(report.student_comments) > 0:
components.append(Spacer(1,0.25*inch))
components.append(Paragraph(_bold_para("Comments"), paragraphStyle))
sc_pairs = _student_comments_to_pairs(report)
_pairs_to_paras(sc_pairs, components, paragraphStyle, bold_headers=False)
#each report starts on new page
components.append(PageBreak())
# generate the PDF
reports_file = StringIO()
document = SimpleDocTemplate(reports_file)
document.build(components)
reports_pdf = reports_file.getvalue()
reports_file.close()
return reports_pdf