root/relatorio/templates/opendocument.py @ 163:fa0cc3bd9a83

Revision 163:fa0cc3bd9a83, 31.1 kB (checked in by C?dric Krier <ced@…>, 3 years ago)

Update manifest.xml with added files in opendocument

Line 
1###############################################################################
2#
3# Copyright (c) 2009 Cedric Krier.
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 3 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__metaclass__ = type
23
24import re
25try:
26    # requires python 2.5+
27    from hashlib import md5
28except ImportError:
29    from md5 import md5
30
31import time
32import urllib
33import zipfile
34from cStringIO import StringIO
35from copy import deepcopy
36
37
38import warnings
39warnings.filterwarnings('always', module='relatorio.templates.opendocument')
40
41import lxml.etree
42import genshi
43import genshi.output
44from genshi.template import MarkupTemplate
45from genshi.filters import Transformer
46from genshi.filters.transform import ENTER, EXIT
47from genshi.core import Stream
48from genshi.template.interpolation import PREFIX
49
50
51from relatorio.templates.base import RelatorioStream
52from relatorio.reporting import Report, MIMETemplateLoader
53try:
54    from relatorio.templates.chart import Template as ChartTemplate
55except ImportError:
56    ChartTemplate = type(None)
57
58GENSHI_EXPR = re.compile(r'''
59        (/)?                                 # is this a closing tag?
60        (for|if|choose|when|otherwise|with)  # tag directive
61        \s*
62        (?:\s(\w+)=["'](.*)["']|$)           # match a single attr & its value
63        |
64        .*                                   # or anything else
65        ''', re.VERBOSE)
66
67EXTENSIONS = {'image/png': 'png',
68              'image/jpeg': 'jpg',
69              'image/bmp': 'bmp',
70              'image/gif': 'gif',
71              'image/tiff': 'tif',
72              'image/xbm': 'xbm',
73             }
74
75RELATORIO_URI = 'http://relatorio.openhex.org/'
76GENSHI_URI = 'http://genshi.edgewall.org/'
77MANIFEST = 'META-INF/manifest.xml'
78output_encode = genshi.output.encode
79EtreeElement = lxml.etree.Element
80
81# A note regarding OpenDocument namespaces:
82#
83# The current code assumes the original OpenOffice document uses default
84# namespace prefix ("table", "xlink", "draw", ...). We derive the actual
85# namespaces URIs from their prefix, instead of the other way round. This has
86# the advantage that if a new version of the format use different namespaces
87# (this is not the case for ODF 1.1 but could be the case in the future since
88# there is a version number in those namespaces after all), Relatorio will
89# support those new formats out of the box.
90
91
92# A note about attribute namespaces:
93#
94# Ideally, we should update the namespace map of all the nodes we add
95# (Genshi) attributes to, so that the attributes use a nice "py" prefix instead
96# of a generated one (eg. "ns0", which is correct but ugly) in the case no
97# parent node defines it. Unfortunately, lxml doesn't support this:
98# the nsmap attribute of Element objects is (currently) readonly.
99
100def guess_type(val):
101    if isinstance(val, (str, unicode)):
102        return 'string'
103    elif isinstance(val, (int, float, long)):
104        return 'float'
105
106class OOTemplateError(genshi.template.base.TemplateSyntaxError):
107    "Error to raise when there is a SyntaxError in the genshi template"
108
109
110class ImageHref:
111    "A class used to add images in the odf zipfile"
112
113    def __init__(self, zfile, manifest, context):
114        self.zip = zfile
115        self.manifest = manifest
116        self.context = context.copy()
117
118    def __call__(self, expr):
119        bitstream, mimetype = expr[:2]
120        if isinstance(bitstream, Report):
121            bitstream = bitstream(**self.context).render()
122        elif isinstance(bitstream, ChartTemplate):
123            bitstream = bitstream.generate(**self.context).render()
124        bitstream.seek(0)
125        file_content = bitstream.read()
126        name = md5(file_content).hexdigest()
127        path = 'Pictures/%s.%s' % (name, EXTENSIONS[mimetype])
128        if path not in self.zip.namelist():
129            self.zip.writestr(path, file_content)
130            self.manifest.add_file_entry(path, mimetype)
131        return {'{http://www.w3.org/1999/xlink}href': path}
132
133
134class ImageDimension:
135    "A class used to set dimension in draw tags"
136
137    def __init__(self, namespaces):
138        self.namespaces = namespaces
139
140    def __call__(self, expr, width, height):
141        # expr could be (bitstream, mimetype)
142        # or (bitstreamm mimetype, width, height)
143        if len(expr) == 4:
144            width, height = expr[2:]
145        attrs = {}
146        if width:
147            attrs['{%s}width' % self.namespaces['svg']] = width
148        if height:
149            attrs['{%s}height' % self.namespaces['svg']] = height
150        return attrs
151
152
153class ColumnCounter:
154    """A class used to count the actual maximum number of cells (and thus
155    columns) a table contains accross its rows.
156    """
157    def __init__(self):
158        self.temp_counters = {}
159        self.counters = {}
160
161    def reset(self, loop_id):
162        self.temp_counters[loop_id] = 0
163
164    def inc(self, loop_id):
165        self.temp_counters[loop_id] += 1
166
167    def store(self, loop_id, table_name):
168        self.counters[table_name] = max(self.temp_counters.pop(loop_id),
169                                        self.counters.get(table_name, 0))
170
171
172def wrap_nodes_between(first, last, new_parent):
173    """An helper function to move all nodes between two nodes to a new node
174    and add that new node to their former parent. The boundary nodes are
175    removed in the process.
176    """
177    old_parent = first.getparent()
178
179    # Any text after the opening tag (and not within a tag) need to be handled
180    # explicitly. For example in <if>xxx<span>yyy</span>zzz</if>, zzz is
181    # copied along the span tag, but not xxx, which corresponds to the tail
182    # attribute of the opening tag.
183    if first.tail:
184        new_parent.text = first.tail
185    for node in first.itersiblings():
186        if node is last:
187            break
188        # appending a node to a new parent also
189        # remove it from its previous parent
190        new_parent.append(node)
191    old_parent.replace(first, new_parent)
192    old_parent.remove(last)
193
194
195def update_py_attrs(node, value):
196    """An helper function to update py_attrs of a node.
197    """
198    if not value:
199        return
200    py_attrs_attr = '{%s}attrs' % GENSHI_URI
201    if not py_attrs_attr in node.attrib:
202        node.attrib[py_attrs_attr] = value
203    else:
204        node.attrib[py_attrs_attr] = \
205                "(lambda x, y: x.update(y) or x)(%s or {}, %s or {})" % \
206                (node.attrib[py_attrs_attr], value)
207
208
209class Template(MarkupTemplate):
210
211    def __init__(self, source, filepath=None, filename=None, loader=None,
212                 encoding=None, lookup='strict', allow_exec=True):
213        self.namespaces = {}
214        self.inner_docs = []
215        self.has_col_loop = False
216        super(Template, self).__init__(source, filepath, filename, loader,
217                                       encoding, lookup, allow_exec)
218
219    def _parse(self, source, encoding):
220        """parses the odf file.
221
222        It adds genshi directives and finds the inner docs.
223        """
224        zf = zipfile.ZipFile(self.filepath)
225        content = zf.read('content.xml')
226        styles = zf.read('styles.xml')
227
228        template = super(Template, self)
229        content = template._parse(self.insert_directives(content), encoding)
230        styles = template._parse(self.insert_directives(styles), encoding)
231        content_files = [('content.xml', content)]
232        styles_files = [('styles.xml', styles)]
233
234        while self.inner_docs:
235            doc = self.inner_docs.pop()
236            c_path, s_path = doc + '/content.xml', doc + '/styles.xml'
237            content = zf.read(c_path)
238            styles = zf.read(s_path)
239
240            c_parsed = template._parse(self.insert_directives(content),
241                                       encoding)
242            s_parsed = template._parse(self.insert_directives(styles),
243                                       encoding)
244            content_files.append((c_path, c_parsed))
245            styles_files.append((s_path, s_parsed))
246
247        zf.close()
248        parsed = []
249        for fpath, fparsed in content_files + styles_files:
250            parsed.append((genshi.core.PI, ('relatorio', fpath), None))
251            parsed += fparsed
252
253        return parsed
254
255    def insert_directives(self, content):
256        """adds the genshi directives, handle the images and the innerdocs.
257        """
258        tree = lxml.etree.parse(StringIO(content))
259        root = tree.getroot()
260
261        # assign default/fake namespaces so that documents do not need to
262        # define them if they don't use them
263        self.namespaces = {
264            "text": "urn:text",
265            "draw": "urn:draw",
266            "table": "urn:table",
267            "office": "urn:office",
268            "xlink": "urn:xlink",
269            "svg": "urn:svg",
270        }
271        # but override them with the real namespaces
272        self.namespaces.update(root.nsmap)
273
274        # remove any "root" namespace as lxml.xpath do not support them
275        self.namespaces.pop(None, None)
276
277        self.namespaces['py'] = GENSHI_URI
278        self.namespaces['relatorio'] = RELATORIO_URI
279
280        self._invert_style(tree)
281        self._handle_relatorio_tags(tree)
282        self._handle_images(tree)
283        self._handle_innerdocs(tree)
284        self._escape_values(tree)
285        return StringIO(lxml.etree.tostring(tree))
286
287    def _invert_style(self, tree):
288        "inverts the text:a and text:span"
289        xpath_expr = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
290                     "/text:span"
291        for span in tree.xpath(xpath_expr, namespaces=self.namespaces):
292            text_a = span.getparent()
293            outer = text_a.getparent()
294            text_a.text = span.text
295            span.text = ''
296            text_a.remove(span)
297            outer.replace(text_a, span)
298            span.append(text_a)
299
300    def _relatorio_statements(self, tree):
301        "finds the relatorio statements (text:a/text:placeholder)"
302        # If this node href matches the relatorio URL it is kept.
303        # If this node href matches a genshi directive it is kept for further
304        # processing.
305        xlink_href_attrib = '{%s}href' % self.namespaces['xlink']
306        text_a = '{%s}a' % self.namespaces['text']
307        placeholder = '{%s}placeholder' % self.namespaces['text']
308        s_xpath = "//text:a[starts-with(@xlink:href, 'relatorio://')]" \
309                  "| //text:placeholder"
310
311        r_statements = []
312        opened_tags = []
313        # We map each opening tag with its closing tag
314        closing_tags = {}
315        for statement in tree.xpath(s_xpath, namespaces=self.namespaces):
316            if statement.tag == placeholder:
317                expr = statement.text[1:-1]
318            elif statement.tag == text_a:
319                expr = urllib.unquote(statement.attrib[xlink_href_attrib][12:])
320
321            if not expr:
322                raise OOTemplateError("No expression in the tag",
323                                      self.filepath)
324            closing, directive, attr, attr_val = \
325                    GENSHI_EXPR.match(expr).groups()
326            is_opening = closing != '/'
327
328            warn_msg = None
329            if not statement.text:
330                warn_msg = "No statement text in '%s' for '%s'" \
331                           % (self.filepath, expr)
332            elif expr != statement.text and statement.tag == text_a:
333                warn_msg = "url and text do not match in %s: %s != %s" \
334                           % (self.filepath, expr,
335                              statement.text.encode('utf-8'))
336            if warn_msg:
337                if directive is not None and not is_opening:
338                    warn_msg += " corresponding to opening tag '%s'" \
339                                % opened_tags[-1].text
340                warnings.warn(warn_msg)
341
342            if directive is not None:
343                # map closing tags with their opening tag
344                if is_opening:
345                    opened_tags.append(statement)
346                else:
347                    closing_tags[id(opened_tags.pop())] = statement
348            # - we only need to return opening statements
349            if is_opening:
350                r_statements.append((statement,
351                                     (expr, directive, attr, attr_val))
352                                   )
353        assert not opened_tags
354        return r_statements, closing_tags
355
356    def _handle_relatorio_tags(self, tree):
357        """
358        Will treat all relatorio tag (py:if/for/choose/when/otherwise)
359        tags
360        """
361        # Some tag/attribute name constants
362        table_namespace = self.namespaces['table']
363        table_row_tag = '{%s}table-row' % table_namespace
364        table_cell_tag = '{%s}table-cell' % table_namespace
365
366        office_name = '{%s}value' % self.namespaces['office']
367        office_valuetype = '{%s}value-type' % self.namespaces['office']
368
369        py_attrs_attr = '{%s}attrs' % GENSHI_URI
370        py_replace = '{%s}replace' % GENSHI_URI
371
372        r_statements, closing_tags = self._relatorio_statements(tree)
373
374        for r_node, parsed in r_statements:
375            expr, directive, attr, a_val = parsed
376
377            # If the node is a genshi directive statement:
378            if directive is not None:
379                opening = r_node
380                closing = closing_tags[id(r_node)]
381
382                # - we find the nearest common ancestor of the closing and
383                #   opening statements
384                o_ancestors = [opening]
385                c_ancestors = [closing] + list(closing.iterancestors())
386                ancestor = None
387                for node in opening.iterancestors():
388                    try:
389                        idx = c_ancestors.index(node)
390                        assert c_ancestors[idx] == node
391                        # we only need ancestors up to the common one
392                        del c_ancestors[idx:]
393                        ancestor = node
394                        break
395                    except ValueError:
396                        # c_ancestors.index(node) raise ValueError if node is
397                        # not a child of c_ancestors
398                        pass
399                    o_ancestors.append(node)
400                assert ancestor is not None, \
401                       "No common ancestor found for opening and closing tag"
402
403                outermost_o_ancestor = o_ancestors[-1]
404                outermost_c_ancestor = c_ancestors[-1]
405
406                # handle horizontal repetitions (over columns)
407                if directive == "for" and ancestor.tag == table_row_tag:
408                    a_val = self._handle_column_loops(parsed, ancestor,
409                                                      opening,
410                                                      outermost_o_ancestor,
411                                                      outermost_c_ancestor)
412
413                # - we create a <py:xxx> node
414                if attr is not None:
415                    attribs = {attr: a_val}
416                else:
417                    attribs = {}
418                genshi_node = EtreeElement('{%s}%s' % (GENSHI_URI,
419                                                       directive),
420                                           attrib=attribs,
421                                           nsmap={'py': GENSHI_URI})
422
423                # - we move all the nodes between the opening and closing
424                #   statements to this new node (append also removes from old
425                #   parent)
426                # - we replace the opening statement by the <py:xxx> node
427                # - we delete the closing statement (and its ancestors)
428                wrap_nodes_between(outermost_o_ancestor, outermost_c_ancestor,
429                                   genshi_node)
430            else:
431                # It's not a genshi statement it's a python expression
432                r_node.attrib[py_replace] = expr
433                parent = r_node.getparent().getparent()
434                if parent is None or parent.tag != table_cell_tag:
435                    continue
436
437                # The grand-parent tag is a table cell we should set the
438                # correct value and type for this cell.
439                dico = "{'%s': %s, '%s': __relatorio_guess_type(%s)}"
440                update_py_attrs(parent, dico %
441                        (office_name, expr, office_valuetype, expr))
442                parent.attrib.pop(office_valuetype, None)
443                parent.attrib.pop(office_name, None)
444
445    def _handle_column_loops(self, statement, ancestor, opening,
446                             outer_o_node, outer_c_node):
447        _, directive, attr, a_val = statement
448
449        self.has_col_loop = True
450
451        table_namespace = self.namespaces['table']
452        table_col_tag = '{%s}table-column' % table_namespace
453        table_num_col_attr = '{%s}number-columns-repeated' % table_namespace
454
455        py_attrs_attr = '{%s}attrs' % GENSHI_URI
456        repeat_tag = '{%s}repeat' % RELATORIO_URI
457
458        # table node (it is not necessarily the direct parent of ancestor)
459        table_node = ancestor.iterancestors('{%s}table' % table_namespace) \
460                             .next()
461        table_name = table_node.attrib['{%s}name' % table_namespace]
462
463        # add counting instructions
464        loop_id = id(opening)
465
466        # 1) add reset counter code on the row opening tag
467        #    (through a py:attrs attribute).
468        # Note that table_name is not needed in the first two
469        # operations, but a unique id within the table is required
470        # to support nested column repetition
471        update_py_attrs(ancestor, "__relatorio_reset_col_count(%d)" % loop_id)
472
473        # 2) add increment code (through a py:attrs attribute) on
474        #    the first cell node after the opening (cell node)
475        #    ancestor
476        enclosed_cell = outer_o_node.getnext()
477        assert enclosed_cell.tag == '{%s}table-cell' % table_namespace
478        update_py_attrs(enclosed_cell, "__relatorio_inc_col_count(%d)" %
479                loop_id)
480
481        # 3) add "store count" code as a py:replace node, as the
482        #    last child of the row
483        attr_value = "__relatorio_store_col_count(%d, %r)" \
484                     % (loop_id, table_name)
485        replace_node = EtreeElement('{%s}replace' % GENSHI_URI,
486                                    attrib={'value': attr_value},
487                                    nsmap={'py': GENSHI_URI})
488        ancestor.append(replace_node)
489
490        # find the position in the row of the cells holding the
491        # <for> and </for> instructions
492        # We use "*" so as to count both normal cells and covered/hidden cells
493        position_xpath_expr = 'count(preceding-sibling::*)'
494        opening_pos = \
495            int(outer_o_node.xpath(position_xpath_expr,
496                                   namespaces=self.namespaces))
497        closing_pos = \
498            int(outer_c_node.xpath(position_xpath_expr,
499                                   namespaces=self.namespaces))
500
501        # check whether or not the opening tag spans several rows
502        a_val = self._handle_row_spanned_column_loops(
503                    statement, outer_o_node, opening_pos, closing_pos)
504
505        # check if this table's headers were already processed
506        repeat_node = table_node.find(repeat_tag)
507        if repeat_node is not None:
508            prev_pos = (int(repeat_node.attrib['opening']),
509                        int(repeat_node.attrib['closing']))
510            if (opening_pos, closing_pos) != prev_pos:
511                raise Exception(
512                    'Incoherent column repetition found! '
513                    'If a table has several lines with repeated '
514                    'columns, the repetition need to be on the '
515                    'same columns across all lines.')
516        else:
517            # compute splits: oo collapses the headers of adjacent
518            # columns which use the same style. We need to split
519            # any column header which is repeated so many times
520            # that it encompasses any of the column headers that
521            # we need to repeat
522            to_split = []
523            idx = 0
524            childs = list(table_node.iterchildren(table_col_tag))
525            for tag in childs:
526                inc = int(tag.attrib.get(table_num_col_attr, 1))
527                oldidx = idx
528                idx += inc
529                if oldidx < opening_pos < idx or \
530                   oldidx < closing_pos < idx:
531                    to_split.append(tag)
532
533            # split tags
534            for tag in to_split:
535                tag_pos = table_node.index(tag)
536                num = int(tag.attrib.pop(table_num_col_attr))
537                new_tags = [deepcopy(tag) for _ in range(num)]
538                table_node[tag_pos:tag_pos+1] = new_tags
539
540            # recompute the list of column headers as it could
541            # have changed.
542            coldefs = list(table_node.iterchildren(table_col_tag))
543
544            # compute the column header nodes corresponding to
545            # the opening and closing tags.
546            first = table_node[opening_pos]
547            last = table_node[closing_pos]
548
549            # add a <relatorio:repeat> node around the column
550            # definitions nodes
551            attribs = {
552               "opening": str(opening_pos),
553               "closing": str(closing_pos),
554               "table": table_name
555            }
556            repeat_node = EtreeElement(repeat_tag, attrib=attribs,
557                                       nsmap={'relatorio': RELATORIO_URI})
558            wrap_nodes_between(first, last, repeat_node)
559        return a_val
560
561    def _handle_row_spanned_column_loops(self, statement, outer_o_node,
562                                         opening_pos, closing_pos):
563        """handles column repetitions which span several rows, by duplicating
564        the py:for node for each row, and make the loops work on a copy of the
565        original iterable as to not exhaust generators."""
566
567        _, directive, attr, a_val = statement
568        table_namespace = self.namespaces['table']
569        table_rowspan_attr = '{%s}number-rows-spanned' % table_namespace
570
571        # checks wether there is a (meaningful) rowspan
572        rows_spanned = int(outer_o_node.attrib.get(table_rowspan_attr, 1))
573        if rows_spanned == 1:
574            return a_val
575
576        table_row_tag = '{%s}table-row' % table_namespace
577        table_cov_cell_tag = '{%s}covered-table-cell' % table_namespace
578
579        # if so, we need to:
580
581        # 1) create a with node to define a temporary variable
582        temp_var = "__relatorio_temp%d" % id(outer_o_node)
583        # a_val == "target in iterable"
584        target, iterable = a_val.split(' in ', 1)
585        vars = "%s = list(%s)" % (temp_var, iterable.strip())
586        with_node = EtreeElement('{%s}with' % GENSHI_URI,
587                                 attrib={"vars": vars},
588                                 nsmap={'py': GENSHI_URI})
589
590        # 2) transform a_val to use that temporary variable
591        a_val = "%s in %s" % (target, temp_var)
592
593        # 3) wrap the corresponding cells on the next row(s)
594        #    (those should be covered-table-cell) inside a
595        #    duplicate py:for node (looping on the temporary
596        #    variable).
597        row_node = outer_o_node.getparent()
598        row_node.addprevious(with_node)
599        rows_to_wrap = [row_node]
600        assert row_node.tag == table_row_tag
601        next_rows = row_node.itersiblings(table_row_tag)
602        for row_idx in range(rows_spanned-1):
603            next_row_node = next_rows.next()
604            rows_to_wrap.append(next_row_node)
605            # compute the start and end nodes
606            first = next_row_node[opening_pos]
607            last = next_row_node[closing_pos]
608            assert first.tag == table_cov_cell_tag
609            assert last.tag == table_cov_cell_tag
610            # wrap them
611            tag = '{%s}%s' % (GENSHI_URI, directive)
612            for_node = EtreeElement(tag,
613                                    attrib={attr: a_val},
614                                    nsmap={'py': GENSHI_URI})
615            wrap_nodes_between(first, last, for_node)
616
617        # 4) wrap all the corresponding rows indide the "with"
618        #    node
619        for node in rows_to_wrap:
620            with_node.append(node)
621        return a_val
622
623    def _handle_images(self, tree):
624        "replaces all draw:frame named 'image: ...' by draw:image nodes"
625        draw_namespace = self.namespaces['draw']
626        draw_name = '{%s}name' % draw_namespace
627        draw_image = '{%s}image' % draw_namespace
628        py_attrs = '{%s}attrs' % self.namespaces['py']
629        svg_namespace = self.namespaces['svg']
630        svg_width = '{%s}width' % svg_namespace
631        svg_height = '{%s}height' % svg_namespace
632        xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]"
633        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
634            d_name = draw.attrib[draw_name][6:].strip()
635            attr_expr = "__relatorio_make_href(%s)" % d_name
636            image_node = EtreeElement(draw_image,
637                                      attrib={py_attrs: attr_expr},
638                                      nsmap={'draw': draw_namespace,
639                                             'py': GENSHI_URI})
640            draw.replace(draw[0], image_node)
641            width = draw.attrib.pop(svg_width, None)
642            height = draw.attrib.pop(svg_height, None)
643            attr_expr = "__relatorio_make_dimension(%s, '%s', '%s')" % \
644                    (d_name, width, height)
645            draw.attrib[py_attrs] = attr_expr
646
647    def _handle_innerdocs(self, tree):
648        "finds inner_docs and adds them to the processing stack."
649        href_attrib = '{%s}href' % self.namespaces['xlink']
650        xpath_expr = "//draw:object[starts-with(@xlink:href, './')" \
651                     "and @xlink:show='embed']"
652        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces):
653            self.inner_docs.append(draw.attrib[href_attrib][2:])
654
655    def _escape_values(self, tree):
656        "escapes element values"
657        for element in tree.iter():
658            for attrs in element.keys():
659                if not attrs.startswith('{%s}' % GENSHI_URI):
660                    element.attrib[attrs] = element.attrib[attrs]\
661                            .replace(PREFIX, PREFIX * 2)
662            if element.text:
663                element.text = element.text.replace(PREFIX, PREFIX * 2)
664
665    def generate(self, *args, **kwargs):
666        "creates the RelatorioStream."
667        serializer = OOSerializer(self.filepath)
668        kwargs['__relatorio_make_href'] = ImageHref(serializer.outzip,
669                                                    serializer.manifest,
670                                                    kwargs)
671        kwargs['__relatorio_make_dimension'] = ImageDimension(self.namespaces)
672        kwargs['__relatorio_guess_type'] = guess_type
673
674        counter = ColumnCounter()
675        kwargs['__relatorio_reset_col_count'] = counter.reset
676        kwargs['__relatorio_inc_col_count'] = counter.inc
677        kwargs['__relatorio_store_col_count'] = counter.store
678
679        stream = super(Template, self).generate(*args, **kwargs)
680        if self.has_col_loop:
681            # Note that we can't simply add a "number-columns-repeated"
682            # attribute and then fill it with the correct number of columns
683            # because that wouldn't work if more than one column is repeated.
684            transformation = DuplicateColumnHeaders(counter)
685            col_filter = Transformer('//repeat[namespace-uri()="%s"]'
686                                     % RELATORIO_URI)
687            col_filter = col_filter.apply(transformation)
688            stream = Stream(list(stream), self.serializer) | col_filter
689        return RelatorioStream(stream, serializer)
690
691
692class DuplicateColumnHeaders(object):
693    def __init__(self, counter):
694        self.counter = counter
695
696    def __call__(self, stream):
697        for mark, (kind, data, pos) in stream:
698            # for each repeat tag found
699            if mark is ENTER:
700                # get the number of columns for that table
701                attrs = data[1]
702                table = attrs.get('table')
703                col_count = self.counter.counters[table]
704
705                # collect events (column header tags) to repeat
706                events = []
707                for submark, event in stream:
708                    if submark is EXIT:
709                        break
710                    events.append(event)
711
712                # repeat them
713                for _ in range(col_count):
714                    for event in events:
715                        yield None, event
716            else:
717                yield mark, (kind, data, pos)
718
719
720class Manifest(object):
721
722    def __init__(self, content):
723        self.tree = lxml.etree.parse(StringIO(content))
724        self.root = self.tree.getroot()
725        self.namespaces = self.root.nsmap
726
727    def __str__(self):
728        return lxml.etree.tostring(self.tree, encoding='UTF-8',
729                                   xml_declaration=True)
730
731    def add_file_entry(self, path, mimetype=None):
732        manifest_namespace = self.namespaces['manifest']
733        attribs = {'media-type': mimetype or '',
734                   'full-path': path}
735        entry_node = EtreeElement('{%s}%s' % (manifest_namespace,
736                                              'file-entry'),
737                                  attrib=attribs,
738                                  nsmap={'manifest': manifest_namespace})
739        self.root.append(entry_node)
740
741
742class OOSerializer:
743
744    def __init__(self, oo_path):
745        self.inzip = zipfile.ZipFile(oo_path)
746        self.manifest = Manifest(self.inzip.read(MANIFEST))
747        self.new_oo = StringIO()
748        self.outzip = zipfile.ZipFile(self.new_oo, 'w')
749        self.xml_serializer = genshi.output.XMLSerializer()
750
751    def __call__(self, stream):
752        files = {}
753        for kind, data, pos in stream:
754            if kind == genshi.core.PI and data[0] == 'relatorio':
755                stream_for = data[1]
756                continue
757            files.setdefault(stream_for, []).append((kind, data, pos))
758
759        now = time.localtime()[:6]
760        for f_info in self.inzip.infolist():
761            if f_info.filename.startswith('ObjectReplacements'):
762                continue
763            elif f_info.filename in files:
764                stream = files[f_info.filename]
765                # create a new file descriptor, copying some attributes from
766                # the original file
767                new_info = zipfile.ZipInfo(f_info.filename, now)
768                for attr in ('compress_type', 'flag_bits', 'create_system'):
769                    setattr(new_info, attr, getattr(f_info, attr))
770                serialized_stream = output_encode(self.xml_serializer(stream))
771                self.outzip.writestr(new_info, serialized_stream)
772            elif f_info.filename == MANIFEST:
773                self.outzip.writestr(f_info, str(self.manifest))
774            else:
775                self.outzip.writestr(f_info, self.inzip.read(f_info.filename))
776        self.inzip.close()
777        self.outzip.close()
778
779        return self.new_oo
780
781MIMETemplateLoader.add_factory('oo.org', Template)
Note: See TracBrowser for help on using the browser.