# 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