| 1 | # -*- encoding: utf-8 -*- |
|---|
| 2 | ############################################################################### |
|---|
| 3 | # |
|---|
| 4 | # Copyright (c) 2007, 2008 OpenHex SPRL. (http://openhex.com) All Rights |
|---|
| 5 | # Reserved. |
|---|
| 6 | # |
|---|
| 7 | # This program is free software; you can redistribute it and/or modify it under |
|---|
| 8 | # the terms of the GNU General Public License as published by the Free Software |
|---|
| 9 | # Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 10 | # version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
|---|
| 15 | # details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License along with |
|---|
| 18 | # this program. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 19 | # |
|---|
| 20 | ############################################################################### |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | import os |
|---|
| 24 | import re |
|---|
| 25 | from cStringIO import StringIO |
|---|
| 26 | |
|---|
| 27 | import lxml.etree |
|---|
| 28 | from nose.tools import * |
|---|
| 29 | import genshi |
|---|
| 30 | from genshi.filters import Translator |
|---|
| 31 | from genshi.core import PI |
|---|
| 32 | from genshi.template.eval import UndefinedError |
|---|
| 33 | |
|---|
| 34 | from templates.opendocument import Template, GENSHI_EXPR, GENSHI_URI, \ |
|---|
| 35 | RELATORIO_URI |
|---|
| 36 | |
|---|
| 37 | OO_TABLE_NS = "urn:oasis:names:tc:opendocument:xmlns:table:1.0" |
|---|
| 38 | |
|---|
| 39 | def pseudo_gettext(string): |
|---|
| 40 | catalog = {'Mes collègues sont:': 'My colleagues are:', |
|---|
| 41 | 'Bonjour,': 'Hello,', |
|---|
| 42 | 'Je suis un test de templating en odt.': |
|---|
| 43 | 'I am an odt templating test', |
|---|
| 44 | 'Felix da housecat': unicode('Félix le chat de la maison', |
|---|
| 45 | 'utf8'), |
|---|
| 46 | 'We sell stuff': u'On vend des choses', |
|---|
| 47 | } |
|---|
| 48 | return catalog.get(string, string) |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | class TestOOTemplating(object): |
|---|
| 52 | |
|---|
| 53 | def setup(self): |
|---|
| 54 | thisdir = os.path.dirname(__file__) |
|---|
| 55 | filepath = os.path.join(thisdir, 'test.odt') |
|---|
| 56 | self.oot = Template(file(filepath), filepath) |
|---|
| 57 | self.data = {'first_name': u'Trente', |
|---|
| 58 | 'last_name': unicode('Møller', 'utf8'), |
|---|
| 59 | 'ville': unicode('Liège', 'utf8'), |
|---|
| 60 | 'friends': [{'first_name': u'Camille', |
|---|
| 61 | 'last_name': u'Salauhpe'}, |
|---|
| 62 | {'first_name': u'Mathias', |
|---|
| 63 | 'last_name': u'Lechat'}], |
|---|
| 64 | 'hobbies': [u'Music', u'Dancing', u'DJing'], |
|---|
| 65 | 'animals': [u'Felix da housecat', u'Dog eat Dog'], |
|---|
| 66 | 'images': [(file(os.path.join(thisdir, 'one.jpg')), |
|---|
| 67 | 'image/jpeg'), |
|---|
| 68 | (file(os.path.join(thisdir, 'two.png')), |
|---|
| 69 | 'image/png')], |
|---|
| 70 | 'oeuf': file(os.path.join(thisdir, 'egg.jpg')), |
|---|
| 71 | 'footer': u'We sell stuff'} |
|---|
| 72 | |
|---|
| 73 | def test_init(self): |
|---|
| 74 | "Testing the correct handling of the styles.xml and content.xml files" |
|---|
| 75 | ok_(isinstance(self.oot.stream, list)) |
|---|
| 76 | eq_(self.oot.stream[0], (PI, ('relatorio', 'content.xml'), None)) |
|---|
| 77 | ok_((PI, ('relatorio', 'content.xml'), None) in self.oot.stream) |
|---|
| 78 | |
|---|
| 79 | def test_directives(self): |
|---|
| 80 | "Testing the directives interpolation" |
|---|
| 81 | xml = '''<xml xmlns:text="urn:text" xmlns:xlink="urn:xlink"> |
|---|
| 82 | <text:a xlink:href="relatorio://foo">foo</text:a> |
|---|
| 83 | </xml>''' |
|---|
| 84 | interpolated = self.oot.insert_directives(xml) |
|---|
| 85 | root_interpolated = lxml.etree.parse(interpolated).getroot() |
|---|
| 86 | child = root_interpolated[0] |
|---|
| 87 | eq_(child.get('{http://genshi.edgewall.org/}replace'), 'foo') |
|---|
| 88 | |
|---|
| 89 | def test_column_looping(self): |
|---|
| 90 | xml = ''' |
|---|
| 91 | <table:table |
|---|
| 92 | xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" |
|---|
| 93 | xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" |
|---|
| 94 | xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" |
|---|
| 95 | xmlns:xlink="http://www.w3.org/1999/xlink" |
|---|
| 96 | table:name="Tableau1" |
|---|
| 97 | table:style-name="Tableau1"> |
|---|
| 98 | <table:table-column table:style-name="Tableau1.A" |
|---|
| 99 | table:number-columns-repeated="2"/> |
|---|
| 100 | <table:table-column table:style-name="Tableau1.C"/> |
|---|
| 101 | <table:table-column table:style-name="Tableau1.A"/> |
|---|
| 102 | <table:table-column table:style-name="Tableau1.E"/> |
|---|
| 103 | <table:table-header-rows> |
|---|
| 104 | <table:table-row table:style-name="Tableau1.1"> |
|---|
| 105 | <table:table-cell table:style-name="Tableau1.A1" |
|---|
| 106 | office:value-type="string"> |
|---|
| 107 | <text:p text:style-name="Table_20_Heading">Brol</text:p> |
|---|
| 108 | </table:table-cell> |
|---|
| 109 | <table:table-cell table:style-name="Tableau1.A1" |
|---|
| 110 | office:value-type="string"> |
|---|
| 111 | <text:p text:style-name="Table_20_Heading"> |
|---|
| 112 | <text:a xlink:type="simple" |
|---|
| 113 | xlink:href="relatorio://for each="title in titles"">for each="title in titles"</text:a> |
|---|
| 114 | </text:p> |
|---|
| 115 | </table:table-cell> |
|---|
| 116 | <table:table-cell table:style-name="Tableau1.A1" |
|---|
| 117 | office:value-type="string"> |
|---|
| 118 | <text:p text:style-name="Table_20_Heading">${title}</text:p> |
|---|
| 119 | <text:p text:style-name="Table_20_Heading"/> |
|---|
| 120 | </table:table-cell> |
|---|
| 121 | <table:table-cell table:style-name="Tableau1.A1" |
|---|
| 122 | office:value-type="string"> |
|---|
| 123 | <text:p text:style-name="Table_20_Heading"> |
|---|
| 124 | <text:a xlink:type="simple" |
|---|
| 125 | xlink:href="relatorio:///for">/for</text:a> |
|---|
| 126 | </text:p> |
|---|
| 127 | </table:table-cell> |
|---|
| 128 | <table:table-cell table:style-name="Tableau1.E1" |
|---|
| 129 | office:value-type="string"> |
|---|
| 130 | <text:p text:style-name="Table_20_Heading">Truc</text:p> |
|---|
| 131 | </table:table-cell> |
|---|
| 132 | </table:table-row> |
|---|
| 133 | </table:table-header-rows> |
|---|
| 134 | <table:table-row> |
|---|
| 135 | <table:table-cell table:style-name="Tableau1.A2" |
|---|
| 136 | table:number-columns-spanned="5" |
|---|
| 137 | office:value-type="string"> |
|---|
| 138 | <text:p text:style-name="Table_20_Contents"> |
|---|
| 139 | <text:a xlink:type="simple" |
|---|
| 140 | xlink:href="relatorio://for%20each=%22items%20in%20lst%22">for each="items in lst"</text:a> |
|---|
| 141 | </text:p> |
|---|
| 142 | </table:table-cell> |
|---|
| 143 | <table:covered-table-cell/> |
|---|
| 144 | <table:covered-table-cell/> |
|---|
| 145 | <table:covered-table-cell/> |
|---|
| 146 | <table:covered-table-cell/> |
|---|
| 147 | </table:table-row> |
|---|
| 148 | <table:table-row> |
|---|
| 149 | <table:table-cell table:style-name="Tableau1.A3" |
|---|
| 150 | office:value-type="string"> |
|---|
| 151 | <text:p text:style-name="Table_20_Contents">Brol</text:p> |
|---|
| 152 | </table:table-cell> |
|---|
| 153 | <table:table-cell table:style-name="Tableau1.A3" |
|---|
| 154 | office:value-type="string"> |
|---|
| 155 | <text:p text:style-name="Table_20_Contents"> |
|---|
| 156 | <text:a xlink:type="simple" |
|---|
| 157 | xlink:href="relatorio://for%20each=%22item%20in%20items%22">for each="item in items"</text:a> |
|---|
| 158 | </text:p> |
|---|
| 159 | </table:table-cell> |
|---|
| 160 | <table:table-cell table:style-name="Tableau1.A3" |
|---|
| 161 | office:value-type="string"> |
|---|
| 162 | <text:p text:style-name="Table_20_Contents">${item}</text:p> |
|---|
| 163 | <text:p text:style-name="Table_20_Contents"/> |
|---|
| 164 | </table:table-cell> |
|---|
| 165 | <table:table-cell table:style-name="Tableau1.A3" |
|---|
| 166 | office:value-type="string"> |
|---|
| 167 | <text:p text:style-name="Table_20_Contents"> |
|---|
| 168 | <text:a xlink:type="simple" |
|---|
| 169 | xlink:href="relatorio:///for">/for</text:a> |
|---|
| 170 | </text:p> |
|---|
| 171 | </table:table-cell> |
|---|
| 172 | <table:table-cell table:style-name="Tableau1.A2" |
|---|
| 173 | office:value-type="string"> |
|---|
| 174 | <text:p text:style-name="Table_20_Contents">Truc</text:p> |
|---|
| 175 | </table:table-cell> |
|---|
| 176 | </table:table-row> |
|---|
| 177 | <table:table-row> |
|---|
| 178 | <table:table-cell table:style-name="Tableau1.A2" |
|---|
| 179 | table:number-columns-spanned="5" |
|---|
| 180 | office:value-type="string"> |
|---|
| 181 | <text:p text:style-name="Table_20_Contents"> |
|---|
| 182 | <text:a xlink:type="simple" |
|---|
| 183 | xlink:href="relatorio:///for">/for</text:a> |
|---|
| 184 | </text:p> |
|---|
| 185 | </table:table-cell> |
|---|
| 186 | <table:covered-table-cell/> |
|---|
| 187 | <table:covered-table-cell/> |
|---|
| 188 | <table:covered-table-cell/> |
|---|
| 189 | <table:covered-table-cell/> |
|---|
| 190 | </table:table-row> |
|---|
| 191 | </table:table>''' |
|---|
| 192 | interpolated = self.oot.insert_directives(xml) |
|---|
| 193 | root = lxml.etree.parse(interpolated).getroot() |
|---|
| 194 | child2 = root[1] |
|---|
| 195 | eq_(child2.tag, "{%s}repeat" % RELATORIO_URI) |
|---|
| 196 | eq_(child2.get("closing"), "3") |
|---|
| 197 | eq_(child2.get("opening"), "1") |
|---|
| 198 | eq_(len(child2), 1) |
|---|
| 199 | child4 = root[3] |
|---|
| 200 | eq_(child4.tag, "{%s}table-header-rows" % OO_TABLE_NS) |
|---|
| 201 | row1 = child4[0] |
|---|
| 202 | ok_(row1.get("{%s}attrs" % GENSHI_URI) |
|---|
| 203 | .startswith('__relatorio_reset_col_count')) |
|---|
| 204 | eq_(len(row1), 4) |
|---|
| 205 | loop = row1[1] |
|---|
| 206 | eq_(loop.tag, "{%s}for" % GENSHI_URI) |
|---|
| 207 | cell = loop[0] |
|---|
| 208 | ok_(cell.get("{%s}attrs" % GENSHI_URI) |
|---|
| 209 | .startswith('__relatorio_inc_col_count')) |
|---|
| 210 | last_row_node = row1[3] |
|---|
| 211 | eq_(last_row_node.tag, "{%s}replace" % GENSHI_URI) |
|---|
| 212 | ok_(last_row_node.get("value") |
|---|
| 213 | .startswith('__relatorio_store_col_count')) |
|---|
| 214 | |
|---|
| 215 | def test_text_outside_p(self): |
|---|
| 216 | "Testing that the tail text of a directive node is handled properly" |
|---|
| 217 | xml = '''<xml xmlns:text="urn:text" xmlns:xlink="urn:xlink"> |
|---|
| 218 | <text:a xlink:href="relatorio://if%20test=%22True%22">if test="True"</text:a> |
|---|
| 219 | xxx |
|---|
| 220 | <text:p text:style-name="other">yyy</text:p> |
|---|
| 221 | zzz |
|---|
| 222 | <text:a xlink:href="relatorio:///if">/if</text:a> |
|---|
| 223 | </xml>''' |
|---|
| 224 | interpolated = self.oot.insert_directives(xml) |
|---|
| 225 | root_interpolated = lxml.etree.parse(interpolated).getroot() |
|---|
| 226 | child = root_interpolated[0] |
|---|
| 227 | eq_(child.tag, '{http://genshi.edgewall.org/}if') |
|---|
| 228 | eq_(child.text.strip(), 'xxx') |
|---|
| 229 | |
|---|
| 230 | def test_styles(self): |
|---|
| 231 | "Testing that styles get rendered" |
|---|
| 232 | stream = self.oot.generate(**self.data) |
|---|
| 233 | rendered = stream.events.render() |
|---|
| 234 | ok_('We sell stuff' in rendered) |
|---|
| 235 | |
|---|
| 236 | dico = self.data.copy() |
|---|
| 237 | del dico['footer'] |
|---|
| 238 | stream = self.oot.generate(**dico) |
|---|
| 239 | assert_raises(UndefinedError, lambda: stream.events.render()) |
|---|
| 240 | |
|---|
| 241 | def test_generate(self): |
|---|
| 242 | "Testing that content get rendered" |
|---|
| 243 | stream = self.oot.generate(**self.data) |
|---|
| 244 | rendered = stream.events.render() |
|---|
| 245 | ok_('Bonjour,' in rendered) |
|---|
| 246 | ok_('Trente' in rendered) |
|---|
| 247 | ok_('Møller' in rendered) |
|---|
| 248 | ok_('Dog eat Dog' in rendered) |
|---|
| 249 | ok_('Felix da housecat' in rendered) |
|---|
| 250 | |
|---|
| 251 | def test_filters(self): |
|---|
| 252 | "Testing the filters with the Translator filter" |
|---|
| 253 | stream = self.oot.generate(**self.data) |
|---|
| 254 | translated = stream.filter(Translator(pseudo_gettext)) |
|---|
| 255 | translated_xml = translated.events.render() |
|---|
| 256 | ok_("Hello," in translated_xml) |
|---|
| 257 | ok_("I am an odt templating test" in translated_xml) |
|---|
| 258 | ok_('Felix da housecat' not in translated_xml) |
|---|
| 259 | ok_('Félix le chat de la maison' in translated_xml) |
|---|
| 260 | ok_('We sell stuff' not in translated_xml) |
|---|
| 261 | ok_('On vend des choses' in translated_xml) |
|---|
| 262 | |
|---|
| 263 | def test_images(self): |
|---|
| 264 | "Testing the image replacement directive" |
|---|
| 265 | stream = self.oot.generate(**self.data) |
|---|
| 266 | rendered = stream.events.render() |
|---|
| 267 | styles_idx = rendered.find('<?relatorio styles.xml?>') |
|---|
| 268 | tree = lxml.etree.parse(StringIO(rendered[25:styles_idx])) |
|---|
| 269 | root = tree.getroot() |
|---|
| 270 | images = root.xpath('//draw:frame', namespaces=self.oot.namespaces) |
|---|
| 271 | eq_(len(images), 3) |
|---|
| 272 | eq_(images[0].get('{%s}name' % self.oot.namespaces['draw']), |
|---|
| 273 | "image: (oeuf, 'image/png')") |
|---|
| 274 | eq_(images[1].get('{%s}name' % self.oot.namespaces['draw']), |
|---|
| 275 | 'image: img') |
|---|
| 276 | eq_(images[1].get('{%s}width' % self.oot.namespaces['svg']), |
|---|
| 277 | '1.732cm') |
|---|
| 278 | eq_(images[1].get('{%s}height' % self.oot.namespaces['svg']), |
|---|
| 279 | '1.513cm') |
|---|
| 280 | eq_(images[2].get('{%s}width' % self.oot.namespaces['svg']), |
|---|
| 281 | '1.732cm') |
|---|
| 282 | eq_(images[2].get('{%s}height' % self.oot.namespaces['svg']), |
|---|
| 283 | '1.513cm') |
|---|
| 284 | |
|---|
| 285 | def test_regexp(self): |
|---|
| 286 | "Testing the regexp used to find relatorio tags" |
|---|
| 287 | # a valid expression |
|---|
| 288 | group = GENSHI_EXPR.match('for each="foo in bar"').groups() |
|---|
| 289 | eq_(group, (None, 'for', 'each', 'foo in bar')) |
|---|
| 290 | |
|---|
| 291 | # invalid expr |
|---|
| 292 | group = GENSHI_EXPR.match('foreach="foo in bar"').groups() |
|---|
| 293 | eq_(group, (None, None, None, None)) |
|---|
| 294 | |
|---|
| 295 | # valid closing tags |
|---|
| 296 | group = GENSHI_EXPR.match('/for').groups() |
|---|
| 297 | eq_(group, ('/', 'for', None, None)) |
|---|
| 298 | group = GENSHI_EXPR.match('/for ').groups() |
|---|
| 299 | eq_(group, ('/', 'for', None, None)) |
|---|
| 300 | |
|---|
| 301 | # another non matching expr |
|---|
| 302 | group = GENSHI_EXPR.match('formatLang("en")').groups() |
|---|
| 303 | eq_(group, (None, None, None, None)) |
|---|
| 304 | |
|---|
| 305 | def test_str(self): |
|---|
| 306 | "Testing that a RelatorioStream str returns a bitstream" |
|---|
| 307 | stream = str(self.oot.generate(**self.data)) |
|---|
| 308 | ok_(isinstance(stream, str)) |
|---|