Merge lp:~justas.sadzevicius/schooltool/schooltool.pdf_testing into lp:~schooltool-owners/schooltool/schooltool

Proposed by Justas Sadzevičius
Status: Merged
Merge reported by: Ignas Mikalajūnas
Merged at revision: not available
Proposed branch: lp:~justas.sadzevicius/schooltool/schooltool.pdf_testing
Merge into: lp:~schooltool-owners/schooltool/schooltool
Diff against target: None lines
To merge this branch: bzr merge lp:~justas.sadzevicius/schooltool/schooltool.pdf_testing
Reviewer Review Type Date Requested Status
Ignas Mikalajūnas (community) Approve
Review via email: mp+5026@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Justas Sadzevičius (justas.sadzevicius) wrote :

For PDF reports: testing helpers for stories of Reportlab platypus flowables.

2467. By Justas Sadzevičius

Refactored helpers to a single StoryXML class.

2468. By Justas Sadzevičius

Whitespace

2469. By Justas Sadzevičius

Improved readability of README.txt and test_pdf a bit.

Revision history for this message
Ignas Mikalajūnas (ignas) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/schooltool/testing/README.txt'
--- src/schooltool/testing/README.txt 2008-01-15 18:46:51 +0000
+++ src/schooltool/testing/README.txt 2009-03-30 12:24:08 +0000
@@ -161,3 +161,60 @@
161 >>> print analyze.queryHTML('/html/body/h1', html)[0]161 >>> print analyze.queryHTML('/html/body/h1', html)[0]
162 <h1>This is my page!</h1>162 <h1>This is my page!</h1>
163163
164
165Reportlab PDF story testing
166---------------------------
167
168Schooltool PDF reports utilize Reportlab platypus module. A report is
169built from a list of platypus flowables known as as 'story'.
170
171Let's build a short pdf story.
172
173 >>> from reportlab.lib.styles import ParagraphStyle
174 >>> from reportlab.platypus.paragraph import Paragraph
175 >>> from reportlab.platypus.flowables import PageBreak
176
177 >>> style = ParagraphStyle(name='Test', fontName='Times-Roman')
178
179 >>> story = [
180 ... Paragraph('Hello world', style),
181 ... PageBreak(),
182 ... Paragraph('A new page', style)]
183
184There are several helpers for testing the stories.
185
186 >>> from schooltool.testing import pdf
187
188The tools aim to build a human readable XML representation of the
189story. There is a helper which prints the formatted XML:
190
191 >>> pdf.printStoryXML(story)
192 <story>
193 <Paragraph>Hello world</Paragraph>
194 <PageBreak/>
195 <Paragraph>A new page</Paragraph>
196 </story>
197
198As with HTML analyzation tools, there are helpers for XPath queries:
199
200 >>> pdf.queryStory('//Paragraph', story)
201 ['<Paragraph>Hello world</Paragraph>',
202 '<Paragraph>A new page</Paragraph>']
203
204 >>> pdf.printQuery('//Paragraph', story)
205 <Paragraph>Hello world</Paragraph>
206 <Paragraph>A new page</Paragraph>
207
208These helpers also work on single platypus flowables:
209
210 >>> pdf.printStoryXML(Paragraph('Some text', style))
211 <story>
212 <Paragraph>Some text</Paragraph>
213 </story>
214
215If these helpers are not sufficient, we can build the raw XML document.
216
217 >>> document = pdf.getStoryXML(story)
218 >>> document
219 <...ElementTree object ...>
220
164221
=== added file 'src/schooltool/testing/pdf.py'
--- src/schooltool/testing/pdf.py 1970-01-01 00:00:00 +0000
+++ src/schooltool/testing/pdf.py 2009-03-30 11:49:24 +0000
@@ -0,0 +1,240 @@
1#
2# SchoolTool - common information systems platform for school administration
3# Copyright (c) 2005 Shuttleworth Foundation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19"""
20SchoolTool Reporlab PDF testing helpers.
21
22$Id$
23
24"""
25
26import cgi
27from cStringIO import StringIO
28from lxml import etree
29
30from reportlab import platypus
31
32
33def getStoryXML(story):
34 """Build human readable XML from a story of Reportlab flowables"""
35 parser = Parser(formatters=_xml_formatters,
36 default=format_classname_xml)
37 text = u'<story>\n%s\n</story>' % parser(story)
38 return etree.parse(StringIO(text.encode('UTF-8')))
39
40
41def printStoryXML(story):
42 print etree.tostring(getStoryXML(story),
43 pretty_print=True)
44
45def queryStory(xpath, story):
46 """Perform an XPath query on XML built from the story of
47 Reportlab flowables"""
48 doc = getStoryXML(story)
49 result = []
50 for node in doc.xpath(xpath):
51 if isinstance(node, basestring):
52 result.append(node)
53 else:
54 result.append(etree.tostring(node, pretty_print=True))
55 return [s.strip() for s in result]
56
57
58def printQuery(xpath, story):
59 """Print results of XPath query on XML built from the story of
60 Reportlab flowables"""
61 for entry in queryStory(xpath, story):
62 print entry
63
64
65null_formatter = lambda parser, flowable: u''
66
67
68class Parser(object):
69 def __init__(self, formatters={}, default=null_formatter):
70 self.formatters = formatters.copy()
71 self.default = default
72
73 def __call__(self, flowable):
74 formatter = self.formatters.get(flowable.__class__, self.default)
75 return formatter(self, flowable)
76
77
78def format_flowable_list(parser, flowables):
79 parsed = [parser(flowable)
80 for flowable in flowables]
81 return '\n'.join([text for text in parsed if text])
82
83
84def format_str_text(parser, flowable):
85 return unicode(flowable)
86
87
88def format_classname_text(parser, flowable):
89 return unicode(flowable.__class__.__name__)
90
91
92def format_str_xml(parser, flowable):
93 return cgi.escape(unicode(flowable))
94
95
96def format_classname_xml(parser, flowable):
97 return u'<%s />' % format_classname_text(parser, flowable)
98
99
100def format_container_xml(parser, flowable):
101 tag_name = format_classname_text(parser, flowable)
102 content = parser(flowable._content)
103 return u'<%s>\n%s\n</%s>' % (tag_name, content, tag_name)
104
105
106def format_preformatted_xml(parser, flowable):
107 tag_name = format_classname_text(parser, flowable)
108 return u'<%s bulletText="%s">%s</%s>' % (
109 tag_name,
110 cgi.escape(flowable.bulletText),
111 cgi.escape(u'\n'.join(flowable.lines)),
112 tag_name)
113
114
115def format_table_xml(parser, flowable):
116 tag_name = format_classname_text(parser, flowable)
117 text = u'<%s>\n' % tag_name
118 for row in flowable._cellvalues:
119 text += '<tr>\n'
120 for cell in row:
121 text += '<td>%s</td>\n' % parser(cell)
122 text += '</tr>\n'
123 text += u'</%s>' % tag_name
124 return text
125
126
127class Format_Attributes_XML(object):
128 def __init__(self, attributes=[], content=''):
129 self.attribute_names = attributes
130 self.content_attribute = content
131
132 def formatAttr(self, parser, flowable, attr_name):
133 words = [word for word in attr_name.split('_') if word]
134 if words:
135 # first word starts with lower case
136 words[0] = words[0][:1].lower() + words[0][1:]
137 # other words start with upper case
138 words[1:] = [word[:1].upper() + word[1:] for word in words[1:]]
139 pretty_name = ''.join(words)
140
141 return u'%s="%s"' % (
142 pretty_name,
143 cgi.escape(str(getattr(flowable, attr_name, None))))
144
145 def formatContents(self, parser, flowable):
146 contents = u''
147 if self.content_attribute:
148 contents = getattr(
149 flowable, self.content_attribute, '')
150 return unicode(cgi.escape(contents))
151
152 def __call__(self, parser, flowable):
153 tag_name = format_classname_text(parser, flowable)
154 text = u'<%s' % tag_name
155 for attr_name in self.attribute_names:
156 text += u' %s' % self.formatAttr(parser, flowable, attr_name)
157
158 contents = self.formatContents(parser, flowable)
159 if contents:
160 text += u'>%s</%s>' % (contents, tag_name)
161 else:
162 text += u' />'
163
164 return text
165
166
167class Format_Paragraph_XML(Format_Attributes_XML):
168 def __init__(self, attributes=[]):
169 Format_Attributes_XML.__init__(self, attributes=attributes)
170
171 def formatContents(self, parser, flowable):
172 return unicode(cgi.escape(flowable.getPlainText()))
173
174
175class Format_ParaAndImage_XML(Format_Attributes_XML):
176
177 def __init__(self):
178 Format_Attributes_XML.__init__(self, ['xpad', 'ypad'])
179
180 def formatContents(self, parser, flowable):
181 text = parser([flowable.I, flowable.P])
182 return text and '\n%s\n' % text or ''
183
184
185_xml_formatters = {
186 # system
187 type(None): null_formatter,
188 list: format_flowable_list,
189
190 # plain text
191 str: format_str_xml,
192 unicode: format_str_xml,
193
194 # paragraph text
195 platypus.paragraph.Paragraph: Format_Paragraph_XML(),
196 platypus.xpreformatted.XPreformatted: Format_Paragraph_XML(
197 attributes=['bulletText']),
198 platypus.xpreformatted.PythonPreformatted: Format_Paragraph_XML(
199 attributes=['bulletText']),
200 platypus.flowables.Preformatted: format_preformatted_xml,
201
202 # graphics
203 platypus.flowables.Image:
204 Format_Attributes_XML(['filename', '_width', '_height']),
205 platypus.flowables.HRFlowable:
206 Format_Attributes_XML(
207 ['width', 'lineWidth', 'spaceBefore', 'spaceAfter',
208 'hAlign', 'vAlign']),
209
210 # containers
211 platypus.tables.Table: format_table_xml,
212 platypus.tables.LongTable: format_table_xml,
213 platypus.flowables.ParagraphAndImage: Format_ParaAndImage_XML(),
214 #platypus.flowables.ImageAndFlowables
215 #platypus.flowables.PTOContainer, # (Please Turn Over The Page behaviour)
216
217 # spacing
218 platypus.flowables.KeepInFrame: format_container_xml,
219 platypus.flowables.KeepTogether: format_container_xml,
220 platypus.flowables.PageBreak: format_classname_xml,
221 platypus.flowables.SlowPageBreak: format_classname_xml,
222 platypus.flowables.CondPageBreak: Format_Attributes_XML(['height']),
223 platypus.flowables.Spacer: Format_Attributes_XML(
224 ['width', 'height']),
225
226 # other
227 platypus.flowables.AnchorFlowable: Format_Attributes_XML(['_name']),
228 #platypus.tableofcontents.TableOfContents,
229 #platypus.tableofcontents.SimpleIndex,
230
231 # omit from output
232 platypus.flowables.UseUpSpace: null_formatter,
233 platypus.flowables.Flowable: null_formatter,
234 platypus.flowables.TraceInfo: null_formatter,
235 platypus.flowables.Macro: null_formatter,
236 platypus.flowables.CallerMacro: null_formatter,
237 platypus.flowables.FailOnWrap: null_formatter,
238 platypus.flowables.FailOnDraw: null_formatter,
239}
240
0241
=== added directory 'src/schooltool/testing/tests'
=== removed file 'src/schooltool/testing/tests.py'
--- src/schooltool/testing/tests.py 2005-10-01 10:25:55 +0000
+++ src/schooltool/testing/tests.py 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
1#
2# SchoolTool - common information systems platform for school administration
3# Copyright (c) 2005 Shuttleworth Foundation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19"""
20Testing the package.
21
22$Id$
23"""
24
25import unittest
26from zope.testing import doctest
27
28
29def test_suite():
30 return unittest.TestSuite((
31 doctest.DocFileSuite('README.txt',
32 optionflags=doctest.NORMALIZE_WHITESPACE),
33 ))
34
35if __name__ == '__main__':
36 unittest.main(default='test_suite')
370
=== added file 'src/schooltool/testing/tests/__init__.py'
--- src/schooltool/testing/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ src/schooltool/testing/tests/__init__.py 2009-03-30 11:49:24 +0000
@@ -0,0 +1,36 @@
1#
2# SchoolTool - common information systems platform for school administration
3# Copyright (c) 2005 Shuttleworth Foundation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19"""
20Testing the package.
21
22$Id$
23"""
24
25import unittest
26from zope.testing import doctest
27
28
29def test_suite():
30 return unittest.TestSuite((
31 doctest.DocFileSuite('../README.txt',
32 optionflags=doctest.NORMALIZE_WHITESPACE),
33 ))
34
35if __name__ == '__main__':
36 unittest.main(default='test_suite')
037
=== added file 'src/schooltool/testing/tests/test_pdf.py'
--- src/schooltool/testing/tests/test_pdf.py 1970-01-01 00:00:00 +0000
+++ src/schooltool/testing/tests/test_pdf.py 2009-03-30 12:24:08 +0000
@@ -0,0 +1,203 @@
1#
2# SchoolTool - common information systems platform for school administration
3# Copyright (c) 2005 Shuttleworth Foundation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19"""
20Tests for pdf testing helpers.
21
22$Id$
23"""
24
25import unittest
26from zope.testing import doctest
27
28from reportlab.lib.styles import ParagraphStyle
29from reportlab.lib import units
30from reportlab import platypus
31
32
33def buildTestStory():
34 para_style = ParagraphStyle(name='Test', fontName='Times-Roman')
35
36 flowables = []
37 flowables.append('Some\ntext')
38
39 flowables.append(
40 platypus.flowables.KeepInFrame(
41 units.inch*2, units.inch, content=[
42 platypus.paragraph.Paragraph('Single line', para_style),
43 u'unicode text']))
44 flowables.append(
45 platypus.xpreformatted.PythonPreformatted('print "foo"',
46 para_style, bulletText='*'))
47 flowables.append(
48 platypus.flowables.KeepTogether([
49 platypus.paragraph.Paragraph(
50 'Multi &amp;\n<b>Line</b>', para_style),
51 platypus.xpreformatted.XPreformatted(
52 'Text', para_style, bulletText='*'),
53 ],
54 maxHeight=units.inch))
55
56 flowables.extend([
57 platypus.flowables.HRFlowable(),
58 platypus.flowables.Image('logo.png', height=units.inch),
59 platypus.flowables.ParagraphAndImage(
60 platypus.paragraph.Paragraph('Text', para_style),
61 platypus.flowables.Image('file.png'),
62 xpad=units.inch),
63 ])
64
65 flowables.extend([
66 platypus.flowables.PageBreak(),
67 platypus.flowables.SlowPageBreak(),
68 platypus.flowables.CondPageBreak(height=units.inch*2),
69 platypus.flowables.Spacer(units.inch*3, units.inch),
70 ])
71
72 flowables.append(platypus.flowables.AnchorFlowable('My anchor'))
73
74 # also add some uninteresting flowables
75 flowables.append(platypus.flowables.UseUpSpace())
76 flowables.append(platypus.flowables.Macro('print "foo"'))
77
78 return flowables
79
80
81def buildTableFlowable():
82 para_style = ParagraphStyle(name='Test', fontName='Times-Roman')
83
84 data = [
85 ['text',
86 platypus.paragraph.Paragraph('Text', para_style)],
87 [['several', 'items in a cell'],
88 platypus.flowables.Image('file.png')],
89 ]
90 return platypus.tables.Table(data)
91
92
93def buildNestedTables():
94 data = [
95 ['A table with another table inside!'],
96 [buildTableFlowable()]]
97 return platypus.tables.LongTable(data)
98
99
100def doctest_XML_building():
101 r"""Tests for getStoryXML and printStoryXML.
102
103 >>> story = buildTestStory()
104
105 getStoryXML builds an XML element tree with some some basic flowable
106 parameters.
107
108 >>> from schooltool.testing.pdf import getStoryXML
109 >>> doc = getStoryXML(story)
110
111 >>> doc
112 <...ElementTree object ...>
113
114 printStoryXML builds and prints the XML tree.
115
116 >>> from schooltool.testing.pdf import printStoryXML
117 >>> printStoryXML(story)
118 <story>
119 Some
120 text
121 <KeepInFrame>
122 <Paragraph>Single line</Paragraph>
123 unicode text
124 </KeepInFrame>
125 <PythonPreformatted bulletText="*">print "foo"</PythonPreformatted>
126 <KeepTogether>
127 <Paragraph>Multi &amp; Line</Paragraph>
128 <XPreformatted bulletText="*">Text</XPreformatted>
129 </KeepTogether>
130 <HRFlowable width="80%" lineWidth="1"
131 spaceBefore="1" spaceAfter="1"
132 hAlign="CENTER" vAlign="BOTTOM"/>
133 <Image filename="logo.png" width="None" height="72.0"/>
134 <ParagraphAndImage xpad="72.0" ypad="3">
135 <Image filename="file.png" width="None" height="None"/>
136 <Paragraph>Text</Paragraph>
137 </ParagraphAndImage>
138 <PageBreak/>
139 <SlowPageBreak/>
140 <CondPageBreak height="144.0"/>
141 <Spacer width="216.0" height="72.0"/>
142 <AnchorFlowable name="My anchor"/>
143 </story>
144
145 Test printing of tables.
146
147 >>> printStoryXML(buildNestedTables())
148 <story>
149 <LongTable>
150 <tr>
151 <td>A table with another table inside!</td>
152 </tr>
153 <tr>
154 <td><Table>
155 <tr>
156 <td>text</td>
157 <td><Paragraph>Text</Paragraph></td>
158 </tr>
159 <tr>
160 <td>several
161 items in a cell</td>
162 <td><Image filename="file.png" width="None" height="None"/></td>
163 </tr>
164 </Table></td>
165 </tr>
166 </LongTable>
167 </story>
168
169 """
170
171
172def doctest_XML_query_helpers():
173 r"""Tests for queryStory and printQuery.
174
175 >>> story = buildTestStory()
176
177 queryStory builds the XML and performs xpath query on it.
178
179 >>> from schooltool.testing.pdf import queryStory
180 >>> queryStory('//Image', story)
181 ['<Image filename="logo.png" width="None" height="72.0"/>',
182 '<Image filename="file.png" width="None" height="None"/>']
183
184 printQuery is a helper which also prints the results:
185
186 >>> from schooltool.testing.pdf import printQuery
187 >>> printQuery('//Image', story)
188 <Image filename="logo.png" width="None" height="72.0"/>
189 <Image filename="file.png" width="None" height="None"/>
190
191 """
192
193
194def test_suite():
195 optionflags = (doctest.NORMALIZE_WHITESPACE |
196 doctest.ELLIPSIS |
197 doctest.REPORT_NDIFF)
198 return unittest.TestSuite((
199 doctest.DocTestSuite(optionflags=optionflags),
200 ))
201
202if __name__ == '__main__':
203 unittest.main(default='test_suite')
0204
=== added file 'src/schooltool/testing/tests/test_readme.py'
--- src/schooltool/testing/tests/test_readme.py 1970-01-01 00:00:00 +0000
+++ src/schooltool/testing/tests/test_readme.py 2009-03-30 12:24:08 +0000
@@ -0,0 +1,39 @@
1#
2# SchoolTool - common information systems platform for school administration
3# Copyright (c) 2005 Shuttleworth Foundation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19"""
20Test suite for README-style tests.
21
22$Id$
23"""
24
25import unittest
26from zope.testing import doctest
27
28
29def test_suite():
30 optionflags = (doctest.NORMALIZE_WHITESPACE |
31 doctest.ELLIPSIS |
32 doctest.REPORT_NDIFF)
33 return unittest.TestSuite((
34 doctest.DocFileSuite('../README.txt',
35 optionflags=optionflags),
36 ))
37
38if __name__ == '__main__':
39 unittest.main(default='test_suite')

Subscribers

People subscribed via source and target branches