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

Proposed by Justas Sadzevičius on 2009-03-30
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 on 2009-04-20
Review via email: mp+5026@code.launchpad.net
To post a comment you must log in.

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

2467. By Justas Sadzevičius on 2009-03-30

Refactored helpers to a single StoryXML class.

2468. By Justas Sadzevičius on 2009-03-30

Whitespace

2469. By Justas Sadzevičius on 2009-03-31

Improved readability of README.txt and test_pdf a bit.

Ignas Mikalajūnas (ignas) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/schooltool/testing/README.txt'
2--- src/schooltool/testing/README.txt 2008-01-15 18:46:51 +0000
3+++ src/schooltool/testing/README.txt 2009-03-30 12:24:08 +0000
4@@ -161,3 +161,60 @@
5 >>> print analyze.queryHTML('/html/body/h1', html)[0]
6 <h1>This is my page!</h1>
7
8+
9+Reportlab PDF story testing
10+---------------------------
11+
12+Schooltool PDF reports utilize Reportlab platypus module. A report is
13+built from a list of platypus flowables known as as 'story'.
14+
15+Let's build a short pdf story.
16+
17+ >>> from reportlab.lib.styles import ParagraphStyle
18+ >>> from reportlab.platypus.paragraph import Paragraph
19+ >>> from reportlab.platypus.flowables import PageBreak
20+
21+ >>> style = ParagraphStyle(name='Test', fontName='Times-Roman')
22+
23+ >>> story = [
24+ ... Paragraph('Hello world', style),
25+ ... PageBreak(),
26+ ... Paragraph('A new page', style)]
27+
28+There are several helpers for testing the stories.
29+
30+ >>> from schooltool.testing import pdf
31+
32+The tools aim to build a human readable XML representation of the
33+story. There is a helper which prints the formatted XML:
34+
35+ >>> pdf.printStoryXML(story)
36+ <story>
37+ <Paragraph>Hello world</Paragraph>
38+ <PageBreak/>
39+ <Paragraph>A new page</Paragraph>
40+ </story>
41+
42+As with HTML analyzation tools, there are helpers for XPath queries:
43+
44+ >>> pdf.queryStory('//Paragraph', story)
45+ ['<Paragraph>Hello world</Paragraph>',
46+ '<Paragraph>A new page</Paragraph>']
47+
48+ >>> pdf.printQuery('//Paragraph', story)
49+ <Paragraph>Hello world</Paragraph>
50+ <Paragraph>A new page</Paragraph>
51+
52+These helpers also work on single platypus flowables:
53+
54+ >>> pdf.printStoryXML(Paragraph('Some text', style))
55+ <story>
56+ <Paragraph>Some text</Paragraph>
57+ </story>
58+
59+If these helpers are not sufficient, we can build the raw XML document.
60+
61+ >>> document = pdf.getStoryXML(story)
62+ >>> document
63+ <...ElementTree object ...>
64+
65
66=== added file 'src/schooltool/testing/pdf.py'
67--- src/schooltool/testing/pdf.py 1970-01-01 00:00:00 +0000
68+++ src/schooltool/testing/pdf.py 2009-03-30 11:49:24 +0000
69@@ -0,0 +1,240 @@
70+#
71+# SchoolTool - common information systems platform for school administration
72+# Copyright (c) 2005 Shuttleworth Foundation
73+#
74+# This program is free software; you can redistribute it and/or modify
75+# it under the terms of the GNU General Public License as published by
76+# the Free Software Foundation; either version 2 of the License, or
77+# (at your option) any later version.
78+#
79+# This program is distributed in the hope that it will be useful,
80+# but WITHOUT ANY WARRANTY; without even the implied warranty of
81+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
82+# GNU General Public License for more details.
83+#
84+# You should have received a copy of the GNU General Public License
85+# along with this program; if not, write to the Free Software
86+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
87+#
88+"""
89+SchoolTool Reporlab PDF testing helpers.
90+
91+$Id$
92+
93+"""
94+
95+import cgi
96+from cStringIO import StringIO
97+from lxml import etree
98+
99+from reportlab import platypus
100+
101+
102+def getStoryXML(story):
103+ """Build human readable XML from a story of Reportlab flowables"""
104+ parser = Parser(formatters=_xml_formatters,
105+ default=format_classname_xml)
106+ text = u'<story>\n%s\n</story>' % parser(story)
107+ return etree.parse(StringIO(text.encode('UTF-8')))
108+
109+
110+def printStoryXML(story):
111+ print etree.tostring(getStoryXML(story),
112+ pretty_print=True)
113+
114+def queryStory(xpath, story):
115+ """Perform an XPath query on XML built from the story of
116+ Reportlab flowables"""
117+ doc = getStoryXML(story)
118+ result = []
119+ for node in doc.xpath(xpath):
120+ if isinstance(node, basestring):
121+ result.append(node)
122+ else:
123+ result.append(etree.tostring(node, pretty_print=True))
124+ return [s.strip() for s in result]
125+
126+
127+def printQuery(xpath, story):
128+ """Print results of XPath query on XML built from the story of
129+ Reportlab flowables"""
130+ for entry in queryStory(xpath, story):
131+ print entry
132+
133+
134+null_formatter = lambda parser, flowable: u''
135+
136+
137+class Parser(object):
138+ def __init__(self, formatters={}, default=null_formatter):
139+ self.formatters = formatters.copy()
140+ self.default = default
141+
142+ def __call__(self, flowable):
143+ formatter = self.formatters.get(flowable.__class__, self.default)
144+ return formatter(self, flowable)
145+
146+
147+def format_flowable_list(parser, flowables):
148+ parsed = [parser(flowable)
149+ for flowable in flowables]
150+ return '\n'.join([text for text in parsed if text])
151+
152+
153+def format_str_text(parser, flowable):
154+ return unicode(flowable)
155+
156+
157+def format_classname_text(parser, flowable):
158+ return unicode(flowable.__class__.__name__)
159+
160+
161+def format_str_xml(parser, flowable):
162+ return cgi.escape(unicode(flowable))
163+
164+
165+def format_classname_xml(parser, flowable):
166+ return u'<%s />' % format_classname_text(parser, flowable)
167+
168+
169+def format_container_xml(parser, flowable):
170+ tag_name = format_classname_text(parser, flowable)
171+ content = parser(flowable._content)
172+ return u'<%s>\n%s\n</%s>' % (tag_name, content, tag_name)
173+
174+
175+def format_preformatted_xml(parser, flowable):
176+ tag_name = format_classname_text(parser, flowable)
177+ return u'<%s bulletText="%s">%s</%s>' % (
178+ tag_name,
179+ cgi.escape(flowable.bulletText),
180+ cgi.escape(u'\n'.join(flowable.lines)),
181+ tag_name)
182+
183+
184+def format_table_xml(parser, flowable):
185+ tag_name = format_classname_text(parser, flowable)
186+ text = u'<%s>\n' % tag_name
187+ for row in flowable._cellvalues:
188+ text += '<tr>\n'
189+ for cell in row:
190+ text += '<td>%s</td>\n' % parser(cell)
191+ text += '</tr>\n'
192+ text += u'</%s>' % tag_name
193+ return text
194+
195+
196+class Format_Attributes_XML(object):
197+ def __init__(self, attributes=[], content=''):
198+ self.attribute_names = attributes
199+ self.content_attribute = content
200+
201+ def formatAttr(self, parser, flowable, attr_name):
202+ words = [word for word in attr_name.split('_') if word]
203+ if words:
204+ # first word starts with lower case
205+ words[0] = words[0][:1].lower() + words[0][1:]
206+ # other words start with upper case
207+ words[1:] = [word[:1].upper() + word[1:] for word in words[1:]]
208+ pretty_name = ''.join(words)
209+
210+ return u'%s="%s"' % (
211+ pretty_name,
212+ cgi.escape(str(getattr(flowable, attr_name, None))))
213+
214+ def formatContents(self, parser, flowable):
215+ contents = u''
216+ if self.content_attribute:
217+ contents = getattr(
218+ flowable, self.content_attribute, '')
219+ return unicode(cgi.escape(contents))
220+
221+ def __call__(self, parser, flowable):
222+ tag_name = format_classname_text(parser, flowable)
223+ text = u'<%s' % tag_name
224+ for attr_name in self.attribute_names:
225+ text += u' %s' % self.formatAttr(parser, flowable, attr_name)
226+
227+ contents = self.formatContents(parser, flowable)
228+ if contents:
229+ text += u'>%s</%s>' % (contents, tag_name)
230+ else:
231+ text += u' />'
232+
233+ return text
234+
235+
236+class Format_Paragraph_XML(Format_Attributes_XML):
237+ def __init__(self, attributes=[]):
238+ Format_Attributes_XML.__init__(self, attributes=attributes)
239+
240+ def formatContents(self, parser, flowable):
241+ return unicode(cgi.escape(flowable.getPlainText()))
242+
243+
244+class Format_ParaAndImage_XML(Format_Attributes_XML):
245+
246+ def __init__(self):
247+ Format_Attributes_XML.__init__(self, ['xpad', 'ypad'])
248+
249+ def formatContents(self, parser, flowable):
250+ text = parser([flowable.I, flowable.P])
251+ return text and '\n%s\n' % text or ''
252+
253+
254+_xml_formatters = {
255+ # system
256+ type(None): null_formatter,
257+ list: format_flowable_list,
258+
259+ # plain text
260+ str: format_str_xml,
261+ unicode: format_str_xml,
262+
263+ # paragraph text
264+ platypus.paragraph.Paragraph: Format_Paragraph_XML(),
265+ platypus.xpreformatted.XPreformatted: Format_Paragraph_XML(
266+ attributes=['bulletText']),
267+ platypus.xpreformatted.PythonPreformatted: Format_Paragraph_XML(
268+ attributes=['bulletText']),
269+ platypus.flowables.Preformatted: format_preformatted_xml,
270+
271+ # graphics
272+ platypus.flowables.Image:
273+ Format_Attributes_XML(['filename', '_width', '_height']),
274+ platypus.flowables.HRFlowable:
275+ Format_Attributes_XML(
276+ ['width', 'lineWidth', 'spaceBefore', 'spaceAfter',
277+ 'hAlign', 'vAlign']),
278+
279+ # containers
280+ platypus.tables.Table: format_table_xml,
281+ platypus.tables.LongTable: format_table_xml,
282+ platypus.flowables.ParagraphAndImage: Format_ParaAndImage_XML(),
283+ #platypus.flowables.ImageAndFlowables
284+ #platypus.flowables.PTOContainer, # (Please Turn Over The Page behaviour)
285+
286+ # spacing
287+ platypus.flowables.KeepInFrame: format_container_xml,
288+ platypus.flowables.KeepTogether: format_container_xml,
289+ platypus.flowables.PageBreak: format_classname_xml,
290+ platypus.flowables.SlowPageBreak: format_classname_xml,
291+ platypus.flowables.CondPageBreak: Format_Attributes_XML(['height']),
292+ platypus.flowables.Spacer: Format_Attributes_XML(
293+ ['width', 'height']),
294+
295+ # other
296+ platypus.flowables.AnchorFlowable: Format_Attributes_XML(['_name']),
297+ #platypus.tableofcontents.TableOfContents,
298+ #platypus.tableofcontents.SimpleIndex,
299+
300+ # omit from output
301+ platypus.flowables.UseUpSpace: null_formatter,
302+ platypus.flowables.Flowable: null_formatter,
303+ platypus.flowables.TraceInfo: null_formatter,
304+ platypus.flowables.Macro: null_formatter,
305+ platypus.flowables.CallerMacro: null_formatter,
306+ platypus.flowables.FailOnWrap: null_formatter,
307+ platypus.flowables.FailOnDraw: null_formatter,
308+}
309+
310
311=== added directory 'src/schooltool/testing/tests'
312=== removed file 'src/schooltool/testing/tests.py'
313--- src/schooltool/testing/tests.py 2005-10-01 10:25:55 +0000
314+++ src/schooltool/testing/tests.py 1970-01-01 00:00:00 +0000
315@@ -1,36 +0,0 @@
316-#
317-# SchoolTool - common information systems platform for school administration
318-# Copyright (c) 2005 Shuttleworth Foundation
319-#
320-# This program is free software; you can redistribute it and/or modify
321-# it under the terms of the GNU General Public License as published by
322-# the Free Software Foundation; either version 2 of the License, or
323-# (at your option) any later version.
324-#
325-# This program is distributed in the hope that it will be useful,
326-# but WITHOUT ANY WARRANTY; without even the implied warranty of
327-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
328-# GNU General Public License for more details.
329-#
330-# You should have received a copy of the GNU General Public License
331-# along with this program; if not, write to the Free Software
332-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
333-#
334-"""
335-Testing the package.
336-
337-$Id$
338-"""
339-
340-import unittest
341-from zope.testing import doctest
342-
343-
344-def test_suite():
345- return unittest.TestSuite((
346- doctest.DocFileSuite('README.txt',
347- optionflags=doctest.NORMALIZE_WHITESPACE),
348- ))
349-
350-if __name__ == '__main__':
351- unittest.main(default='test_suite')
352
353=== added file 'src/schooltool/testing/tests/__init__.py'
354--- src/schooltool/testing/tests/__init__.py 1970-01-01 00:00:00 +0000
355+++ src/schooltool/testing/tests/__init__.py 2009-03-30 11:49:24 +0000
356@@ -0,0 +1,36 @@
357+#
358+# SchoolTool - common information systems platform for school administration
359+# Copyright (c) 2005 Shuttleworth Foundation
360+#
361+# This program is free software; you can redistribute it and/or modify
362+# it under the terms of the GNU General Public License as published by
363+# the Free Software Foundation; either version 2 of the License, or
364+# (at your option) any later version.
365+#
366+# This program is distributed in the hope that it will be useful,
367+# but WITHOUT ANY WARRANTY; without even the implied warranty of
368+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
369+# GNU General Public License for more details.
370+#
371+# You should have received a copy of the GNU General Public License
372+# along with this program; if not, write to the Free Software
373+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
374+#
375+"""
376+Testing the package.
377+
378+$Id$
379+"""
380+
381+import unittest
382+from zope.testing import doctest
383+
384+
385+def test_suite():
386+ return unittest.TestSuite((
387+ doctest.DocFileSuite('../README.txt',
388+ optionflags=doctest.NORMALIZE_WHITESPACE),
389+ ))
390+
391+if __name__ == '__main__':
392+ unittest.main(default='test_suite')
393
394=== added file 'src/schooltool/testing/tests/test_pdf.py'
395--- src/schooltool/testing/tests/test_pdf.py 1970-01-01 00:00:00 +0000
396+++ src/schooltool/testing/tests/test_pdf.py 2009-03-30 12:24:08 +0000
397@@ -0,0 +1,203 @@
398+#
399+# SchoolTool - common information systems platform for school administration
400+# Copyright (c) 2005 Shuttleworth Foundation
401+#
402+# This program is free software; you can redistribute it and/or modify
403+# it under the terms of the GNU General Public License as published by
404+# the Free Software Foundation; either version 2 of the License, or
405+# (at your option) any later version.
406+#
407+# This program is distributed in the hope that it will be useful,
408+# but WITHOUT ANY WARRANTY; without even the implied warranty of
409+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
410+# GNU General Public License for more details.
411+#
412+# You should have received a copy of the GNU General Public License
413+# along with this program; if not, write to the Free Software
414+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
415+#
416+"""
417+Tests for pdf testing helpers.
418+
419+$Id$
420+"""
421+
422+import unittest
423+from zope.testing import doctest
424+
425+from reportlab.lib.styles import ParagraphStyle
426+from reportlab.lib import units
427+from reportlab import platypus
428+
429+
430+def buildTestStory():
431+ para_style = ParagraphStyle(name='Test', fontName='Times-Roman')
432+
433+ flowables = []
434+ flowables.append('Some\ntext')
435+
436+ flowables.append(
437+ platypus.flowables.KeepInFrame(
438+ units.inch*2, units.inch, content=[
439+ platypus.paragraph.Paragraph('Single line', para_style),
440+ u'unicode text']))
441+ flowables.append(
442+ platypus.xpreformatted.PythonPreformatted('print "foo"',
443+ para_style, bulletText='*'))
444+ flowables.append(
445+ platypus.flowables.KeepTogether([
446+ platypus.paragraph.Paragraph(
447+ 'Multi &amp;\n<b>Line</b>', para_style),
448+ platypus.xpreformatted.XPreformatted(
449+ 'Text', para_style, bulletText='*'),
450+ ],
451+ maxHeight=units.inch))
452+
453+ flowables.extend([
454+ platypus.flowables.HRFlowable(),
455+ platypus.flowables.Image('logo.png', height=units.inch),
456+ platypus.flowables.ParagraphAndImage(
457+ platypus.paragraph.Paragraph('Text', para_style),
458+ platypus.flowables.Image('file.png'),
459+ xpad=units.inch),
460+ ])
461+
462+ flowables.extend([
463+ platypus.flowables.PageBreak(),
464+ platypus.flowables.SlowPageBreak(),
465+ platypus.flowables.CondPageBreak(height=units.inch*2),
466+ platypus.flowables.Spacer(units.inch*3, units.inch),
467+ ])
468+
469+ flowables.append(platypus.flowables.AnchorFlowable('My anchor'))
470+
471+ # also add some uninteresting flowables
472+ flowables.append(platypus.flowables.UseUpSpace())
473+ flowables.append(platypus.flowables.Macro('print "foo"'))
474+
475+ return flowables
476+
477+
478+def buildTableFlowable():
479+ para_style = ParagraphStyle(name='Test', fontName='Times-Roman')
480+
481+ data = [
482+ ['text',
483+ platypus.paragraph.Paragraph('Text', para_style)],
484+ [['several', 'items in a cell'],
485+ platypus.flowables.Image('file.png')],
486+ ]
487+ return platypus.tables.Table(data)
488+
489+
490+def buildNestedTables():
491+ data = [
492+ ['A table with another table inside!'],
493+ [buildTableFlowable()]]
494+ return platypus.tables.LongTable(data)
495+
496+
497+def doctest_XML_building():
498+ r"""Tests for getStoryXML and printStoryXML.
499+
500+ >>> story = buildTestStory()
501+
502+ getStoryXML builds an XML element tree with some some basic flowable
503+ parameters.
504+
505+ >>> from schooltool.testing.pdf import getStoryXML
506+ >>> doc = getStoryXML(story)
507+
508+ >>> doc
509+ <...ElementTree object ...>
510+
511+ printStoryXML builds and prints the XML tree.
512+
513+ >>> from schooltool.testing.pdf import printStoryXML
514+ >>> printStoryXML(story)
515+ <story>
516+ Some
517+ text
518+ <KeepInFrame>
519+ <Paragraph>Single line</Paragraph>
520+ unicode text
521+ </KeepInFrame>
522+ <PythonPreformatted bulletText="*">print "foo"</PythonPreformatted>
523+ <KeepTogether>
524+ <Paragraph>Multi &amp; Line</Paragraph>
525+ <XPreformatted bulletText="*">Text</XPreformatted>
526+ </KeepTogether>
527+ <HRFlowable width="80%" lineWidth="1"
528+ spaceBefore="1" spaceAfter="1"
529+ hAlign="CENTER" vAlign="BOTTOM"/>
530+ <Image filename="logo.png" width="None" height="72.0"/>
531+ <ParagraphAndImage xpad="72.0" ypad="3">
532+ <Image filename="file.png" width="None" height="None"/>
533+ <Paragraph>Text</Paragraph>
534+ </ParagraphAndImage>
535+ <PageBreak/>
536+ <SlowPageBreak/>
537+ <CondPageBreak height="144.0"/>
538+ <Spacer width="216.0" height="72.0"/>
539+ <AnchorFlowable name="My anchor"/>
540+ </story>
541+
542+ Test printing of tables.
543+
544+ >>> printStoryXML(buildNestedTables())
545+ <story>
546+ <LongTable>
547+ <tr>
548+ <td>A table with another table inside!</td>
549+ </tr>
550+ <tr>
551+ <td><Table>
552+ <tr>
553+ <td>text</td>
554+ <td><Paragraph>Text</Paragraph></td>
555+ </tr>
556+ <tr>
557+ <td>several
558+ items in a cell</td>
559+ <td><Image filename="file.png" width="None" height="None"/></td>
560+ </tr>
561+ </Table></td>
562+ </tr>
563+ </LongTable>
564+ </story>
565+
566+ """
567+
568+
569+def doctest_XML_query_helpers():
570+ r"""Tests for queryStory and printQuery.
571+
572+ >>> story = buildTestStory()
573+
574+ queryStory builds the XML and performs xpath query on it.
575+
576+ >>> from schooltool.testing.pdf import queryStory
577+ >>> queryStory('//Image', story)
578+ ['<Image filename="logo.png" width="None" height="72.0"/>',
579+ '<Image filename="file.png" width="None" height="None"/>']
580+
581+ printQuery is a helper which also prints the results:
582+
583+ >>> from schooltool.testing.pdf import printQuery
584+ >>> printQuery('//Image', story)
585+ <Image filename="logo.png" width="None" height="72.0"/>
586+ <Image filename="file.png" width="None" height="None"/>
587+
588+ """
589+
590+
591+def test_suite():
592+ optionflags = (doctest.NORMALIZE_WHITESPACE |
593+ doctest.ELLIPSIS |
594+ doctest.REPORT_NDIFF)
595+ return unittest.TestSuite((
596+ doctest.DocTestSuite(optionflags=optionflags),
597+ ))
598+
599+if __name__ == '__main__':
600+ unittest.main(default='test_suite')
601
602=== added file 'src/schooltool/testing/tests/test_readme.py'
603--- src/schooltool/testing/tests/test_readme.py 1970-01-01 00:00:00 +0000
604+++ src/schooltool/testing/tests/test_readme.py 2009-03-30 12:24:08 +0000
605@@ -0,0 +1,39 @@
606+#
607+# SchoolTool - common information systems platform for school administration
608+# Copyright (c) 2005 Shuttleworth Foundation
609+#
610+# This program is free software; you can redistribute it and/or modify
611+# it under the terms of the GNU General Public License as published by
612+# the Free Software Foundation; either version 2 of the License, or
613+# (at your option) any later version.
614+#
615+# This program is distributed in the hope that it will be useful,
616+# but WITHOUT ANY WARRANTY; without even the implied warranty of
617+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
618+# GNU General Public License for more details.
619+#
620+# You should have received a copy of the GNU General Public License
621+# along with this program; if not, write to the Free Software
622+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
623+#
624+"""
625+Test suite for README-style tests.
626+
627+$Id$
628+"""
629+
630+import unittest
631+from zope.testing import doctest
632+
633+
634+def test_suite():
635+ optionflags = (doctest.NORMALIZE_WHITESPACE |
636+ doctest.ELLIPSIS |
637+ doctest.REPORT_NDIFF)
638+ return unittest.TestSuite((
639+ doctest.DocFileSuite('../README.txt',
640+ optionflags=optionflags),
641+ ))
642+
643+if __name__ == '__main__':
644+ unittest.main(default='test_suite')

Subscribers

People subscribed via source and target branches