Changeset 93:12bf59edd59e

Show
Ignore:
Timestamp:
01/20/09 09:39:27 (14 months ago)
Author:
Ga?tan de Menten <ged@…>
Branch:
default
Message:

- implement correct column looping
- warning messages now include more information

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • relatorio/templates/opendocument.py

    r91 r93  
    3636import genshi.output 
    3737from genshi.template import MarkupTemplate 
     38from genshi.filters import Transformer 
     39from genshi.filters.transform import ENTER, EXIT 
     40from genshi.core import Stream 
     41 
    3842 
    3943from relatorio.templates.base import RelatorioStream 
     
    4650GENSHI_EXPR = re.compile(r''' 
    4751        (/)?                                 # is this a closing tag? 
    48         (for|choose|otherwise|when|if|with)  # tag directive 
     52        (for|if|choose|when|otherwise|with)  # tag directive 
    4953        \s* 
    5054        (?:\s(\w+)=["'](.*)["']|$)           # match a single attr & its value 
     
    6165             } 
    6266 
     67RELATORIO_URI = 'http://relatorio.openhex.org/' 
    6368output_encode = genshi.output.encode 
    6469EtreeElement = lxml.etree.Element 
     
    96101        return {'{http://www.w3.org/1999/xlink}href': path} 
    97102 
    98 class TableColumnDef: 
     103 
     104class ColumnCounter: 
    99105    """A class used to add the correct number of column definitions to a 
    100106    table containing an horizontal repetition" 
    101107    """ 
    102     def __init__(self, zfile, context): 
    103         self.zip = zfile 
    104         self.context = context.copy() 
    105  
    106     def __call__(self, expr, name): 
    107         #FIXME: name argument is unused 
    108         return 
     108    def __init__(self): 
     109        self.temp_counters = {} 
     110        self.counters = {} 
     111 
     112    def reset(self, loop_id): 
     113        self.temp_counters[loop_id] = 0 
     114 
     115    def inc(self, loop_id): 
     116        self.temp_counters[loop_id] += 1 
     117 
     118    def store(self, loop_id, table_name): 
     119        self.counters[table_name] = max(self.temp_counters.pop(loop_id), 
     120                                        self.counters.get(table_name, 0)) 
     121 
    109122 
    110123class Template(MarkupTemplate): 
     
    114127        self.namespaces = {} 
    115128        self.inner_docs = [] 
     129        self.has_col_loop = False 
    116130        super(Template, self).__init__(source, filepath, filename, loader, 
    117131                                       encoding, lookup, allow_exec) 
     
    160174        self.namespaces = root.nsmap.copy() 
    161175        self.namespaces['py'] = 'http://genshi.edgewall.org/' 
     176        self.namespaces['relatorio'] = RELATORIO_URI 
    162177 
    163178        self._invert_style(tree) 
     
    204219                raise OOTemplateError("No expression in the tag", 
    205220                                      self.filepath) 
    206             elif not statement.text: 
    207                 warnings.warn('No statement text in %s' % self.filepath) 
    208             elif expr != statement.text and statement.tag == text_a: 
    209                 warnings.warn('url and text do not match in %s: %s != %s' 
    210                               % (self.filepath, expr, 
    211                                  statement.text.encode('utf-8'))) 
    212  
    213221            closing, directive, attr, attr_val = \ 
    214222                    GENSHI_EXPR.match(expr).groups() 
    215223            is_opening = closing != '/' 
     224 
     225            warn_msg = None 
     226            if not statement.text: 
     227                warn_msg = "No statement text in '%s' for '%s'" \ 
     228                           % (self.filepath, expr) 
     229            elif expr != statement.text and statement.tag == text_a: 
     230                warn_msg = "url and text do not match in %s: %s != %s" \ 
     231                           % (self.filepath, expr, 
     232                              statement.text.encode('utf-8')) 
     233            if warn_msg: 
     234                if directive is not None and not is_opening: 
     235                    warn_msg += " corresponding to opening tag '%s'" \ 
     236                                % opened_tags[-1].text 
     237                warnings.warn(warn_msg) 
     238 
    216239            if directive is not None: 
    217240                # map closing tags with their opening tag 
     
    234257        """ 
    235258        # Some tag/attribute name constants 
    236         table_cell_tag = '{%s}table-cell' % self.namespaces['table'] 
    237         table_row_tag = '{%s}table-row' % self.namespaces['table'] 
    238         num_col_attr = '{%s}number-columns-repeated' % self.namespaces['table'] 
    239         attrib_name = '{%s}attrs' % self.namespaces['py'] 
     259        table_namespace = self.namespaces['table'] 
     260        table_tag = '{%s}table' % table_namespace 
     261        table_col_tag = '{%s}table-column' % table_namespace 
     262        table_row_tag = '{%s}table-row' % table_namespace 
     263        table_cell_tag = '{%s}table-cell' % table_namespace 
     264        table_num_col_attr = '{%s}number-columns-repeated' % table_namespace 
     265        table_name_attr = '{%s}name' % table_namespace 
     266 
    240267        office_name = '{%s}value' % self.namespaces['office'] 
    241268        office_valuetype = '{%s}value-type' % self.namespaces['office'] 
     269 
    242270        py_namespace = self.namespaces['py'] 
     271        py_attrs_attr = '{%s}attrs' % py_namespace 
    243272        genshi_replace = '{%s}replace' % py_namespace 
     273 
     274        repeat_tag = '{%s}repeat' % self.namespaces['relatorio'] 
    244275 
    245276        r_statements, closing_tags = self._relatorio_statements(tree) 
     
    271302                assert ancestor is not None, \ 
    272303                       "No common ancestor found for opening and closing tag" 
     304                outermost_o_ancestor = o_ancestors[-1] 
     305                outermost_c_ancestor = c_ancestors[-1] 
    273306 
    274307                # handle horizontal repetition (over columns) 
    275                 if False: 
    276                 #if directive == "for" and ancestor.tag == table_row_tag: 
    277                     print "horizontal repetition", a_val 
    278                     # find position of current cell in row 
    279                     position_xpath_expr = \ 
    280                     'count(ancestor::table:table-cell/preceding-sibling::*)' 
    281                     opening_pos = opening.xpath(position_xpath_expr, 
    282                                                 namespaces=self.namespaces) 
    283                     closing_pos = closing.xpath(position_xpath_expr, 
    284                                                 namespaces=self.namespaces) 
    285                     print "opening_pos", opening_pos 
    286                     print "closing_pos", closing_pos 
    287  
    288                     idx = 0 
    289                     table_node = opening.xpath('ancestor::table:table[1]', 
    290                                                namespaces=self.namespaces)[0] 
    291                     #XXX: use getiterator('table:table-column') instead of xpath? 
    292                     to_split = [] 
    293                     to_move = [] 
    294                     for tag in table_node.xpath('table:table-column', 
    295                                                 namespaces=self.namespaces): 
    296                         if num_col_attr in tag.attrib: 
    297                             oldidx = idx 
    298                             idx += int(tag.attrib[num_col_attr]) 
    299                             print "oldidx", oldidx, "idx", idx 
    300                             if oldidx < opening_pos < idx or \ 
    301                                oldidx < closing_pos < idx: 
    302                                 to_split.append(tag) 
    303                                 print "to_split", to_split 
    304                         else: 
    305                             idx += 1 
    306  
    307                     # split tags 
    308                     for tag in to_split: 
    309                         tag_pos = table_node.index(tag) 
    310                         print "tag_pos", tag_pos 
    311                         num = int(tag.attrib[num_col_attr]) 
    312                         tag.attrib.pop(num_col_attr) 
    313                         new_tags = [deepcopy(tag) for _ in range(num)] 
    314                         table_node[tag_pos:tag_pos] = new_tags 
    315  
    316                     # compute moves 
    317                     if False: 
    318                         if idx < opening_pos: 
    319                             pass 
    320                         elif opening_pos < idx < closing_pos: 
    321                             to_move.append(tag) 
    322                         else: 
    323                             break 
    324                         print idx 
    325                     # move tags 
    326                     # 
    327                     # add a <py:for each="%s"> % a_val 
    328 #                    for_node = EtreeElement('{%s}%s' % (py_namespace, 'for'), 
    329 #                                            attrib={attr: a_val}, 
    330 #                                            nsmap=self.namespaces) 
     308                if directive == "for" and ancestor.tag == table_row_tag: 
     309                    self.has_col_loop = True 
     310 
     311                    # table (it is not necessarily the parent of ancestor) 
     312                    table_node = ancestor.iterancestors(table_tag).next() 
     313                    table_name = table_node.attrib[table_name_attr] 
     314 
     315                    # add counting instructions 
     316                    loop_id = id(opening) 
     317 
     318                    # 1) add reset counter code on the row opening tag 
     319                    #    (through a py:attrs attribute). 
     320                    # Note that table_name is not needed in the first two 
     321                    # operations, but a unique id within the table is required 
     322                    # to support nested column repetition 
     323                    attr_value = "__reset_col_count(%d)" % loop_id 
     324                    ancestor.attrib[py_attrs_attr] = attr_value 
     325 
     326                    # 2) add increment code (through a py:attrs attribute) on 
     327                    #    the first cell node after the opening (cell node) 
     328                    #    ancestor 
     329                    enclosed_cell = outermost_o_ancestor.itersiblings().next() 
     330                    assert enclosed_cell.tag == table_cell_tag 
     331                    attr_value = "__inc_col_count(%d)" % loop_id 
     332                    enclosed_cell.attrib[py_attrs_attr] = attr_value 
     333 
     334                    # 3) add "store count" code as a py:replace node, as the 
     335                    #    last child of the row 
     336                    attr_value = "__store_col_count(%d, %r)" % (loop_id, 
     337                                                                table_name) 
     338                    replace_node = EtreeElement('{%s}replace' % py_namespace, 
     339                                                attrib={'value': attr_value}, 
     340                                                nsmap=self.namespaces) 
     341                    ancestor.append(replace_node) 
     342 
     343                    # find the position in the row of the cells holding the 
     344                    # <for> and </for> instructions 
     345                    position_xpath_expr = 'count(preceding-sibling::*)' 
     346                    opening_pos = \ 
     347                        int(outermost_o_ancestor.xpath(position_xpath_expr, 
     348                                                   namespaces=self.namespaces)) 
     349                    closing_pos = \ 
     350                        int(outermost_c_ancestor.xpath(position_xpath_expr, 
     351                                                   namespaces=self.namespaces)) 
     352 
     353                    # check if this table was already processed 
     354                    repeat_node = table_node.find(repeat_tag) 
     355                    if repeat_node is not None: 
     356                        prev_pos = (int(repeat_node.attrib['opening']), 
     357                                    int(repeat_node.attrib['closing'])) 
     358                        if (opening_pos, closing_pos) != prev_pos: 
     359                            raise Exception( 
     360                                'Incoherent column repetition found! ' 
     361                                'If a table has several lines with repeated ' 
     362                                'columns, the repetition need to be on the ' 
     363                                'same columns across all lines.') 
     364                    else: 
     365                        # compute splits: we need to split any column header 
     366                        # which is repeated so many times that it encompass 
     367                        # any of the column headers that we need to repeat 
     368                        to_split = [] 
     369                        idx = 0 
     370                        childs = list(table_node.iterchildren(table_col_tag)) 
     371                        assert closing_pos < len(childs) 
     372                        for tag in childs: 
     373                            if table_num_col_attr in tag.attrib: 
     374                                oldidx = idx 
     375                                idx += int(tag.attrib[table_num_col_attr]) 
     376                                if oldidx < opening_pos < idx or \ 
     377                                   oldidx < closing_pos < idx: 
     378                                    to_split.append(tag) 
     379                            else: 
     380                                idx += 1 
     381 
     382                        # split tags 
     383                        for tag in to_split: 
     384                            tag_pos = table_node.index(tag) 
     385                            num = int(tag.attrib.pop(table_num_col_attr)) 
     386                            new_tags = [deepcopy(tag) for _ in range(num)] 
     387                            table_node[tag_pos:tag_pos+1] = new_tags 
     388 
     389                        # compute moves and deletes: the column headers 
     390                        # corresponding to the opening and closing tags 
     391                        # need to be deleted. The column headers between them 
     392                        # need to be moved to inside the "repeat" tag (which 
     393                        # is to be created later). 
     394                        to_remove = [] 
     395                        to_move = [] 
     396                        coldefs = table_node.iterchildren(table_col_tag) 
     397                        for idx, tag in enumerate(coldefs): 
     398                            if idx in (opening_pos, closing_pos): 
     399                                to_remove.append(tag) 
     400                            if opening_pos < idx < closing_pos: 
     401                                to_move.append(tag) 
     402                            elif idx > closing_pos: 
     403                                break 
     404                        assert len(to_remove) == 2, "failed %d %d" \ 
     405                               % (opening_pos, closing_pos) 
     406                        assert to_move 
     407 
     408                        # execute deletes 
     409                        for node in to_remove: 
     410                            table_node.remove(node) 
     411 
     412                        # execute moves (add a <relatorio:repeat> node around 
     413                        # the column definitions nodes) 
     414                        o_pos, c_pos = str(opening_pos), str(closing_pos) 
     415                        repeat_node = EtreeElement(repeat_tag, 
     416                                                   attrib={ 
     417                                                       "opening": o_pos, 
     418                                                       "closing": c_pos, 
     419                                                       "table": table_name}, 
     420                                                   nsmap=self.namespaces) 
     421 
     422                        for node in to_move[1:]: 
     423                            table_node.remove(node) 
     424                            repeat_node.append(node) 
     425                        table_node.replace(to_move[0], repeat_node) 
     426                        repeat_node.append(to_move[0]) 
    331427 
    332428                # - we create a <py:xxx> node 
     
    338434                # - we add all the nodes between the opening and closing 
    339435                #   statements to this new node 
    340                 outermost_o_ancestor = o_ancestors[-1] 
    341                 outermost_c_ancestor = c_ancestors[-1] 
    342436                for node in outermost_o_ancestor.itersiblings(): 
    343437                    if node is outermost_c_ancestor: 
     
    360454                # correct value and type for this cell. 
    361455                dico = "{'%s': %s, '%s': guess_type(%s)}" 
    362                 parent.attrib[attrib_name] = dico % (office_name, expr, 
     456                parent.attrib[py_attrs_attr] = dico % (office_name, expr, 
    363457                                                     office_valuetype, expr) 
    364458                parent.attrib.pop(office_valuetype, None) 
     
    369463        draw_name = '{%s}name' % self.namespaces['draw'] 
    370464        draw_image = '{%s}image' % self.namespaces['draw'] 
    371         python_attrs = '{%s}attrs' % self.namespaces['py'] 
     465        py_attrs = '{%s}attrs' % self.namespaces['py'] 
    372466        xpath_expr = "//draw:frame[starts-with(@draw:name, 'image:')]" 
    373467        for draw in tree.xpath(xpath_expr, namespaces=self.namespaces): 
     
    375469            attr_expr = "make_href(%s, %r)" % (d_name[7:], d_name[7:]) 
    376470            image_node = EtreeElement(draw_image, 
    377                                       attrib={python_attrs: attr_expr}, 
     471                                      attrib={py_attrs: attr_expr}, 
    378472                                      nsmap=self.namespaces) 
    379473            draw.replace(draw[0], image_node) 
     
    392486        kwargs['make_href'] = ImageHref(serializer.outzip, kwargs) 
    393487        kwargs['guess_type'] = guess_type 
    394         generate_all = super(Template, self).generate(*args, **kwargs) 
    395  
    396         return RelatorioStream(generate_all, serializer) 
     488 
     489        counter = ColumnCounter() 
     490        kwargs['__reset_col_count'] = counter.reset 
     491        kwargs['__inc_col_count'] = counter.inc 
     492        kwargs['__store_col_count'] = counter.store 
     493 
     494        stream = super(Template, self).generate(*args, **kwargs) 
     495        if self.has_col_loop: 
     496            transformation = DuplicateColumnHeaders(counter) 
     497            col_filter = Transformer('//repeat[namespace-uri()="%s"]' 
     498                                     % RELATORIO_URI) 
     499            col_filter = col_filter.apply(transformation) 
     500            stream = Stream(list(stream), self.serializer) | col_filter 
     501        return RelatorioStream(stream, serializer) 
     502 
     503 
     504class DuplicateColumnHeaders(object): 
     505    def __init__(self, counter): 
     506        self.counter = counter 
     507 
     508    def __call__(self, stream): 
     509        for mark, (kind, data, pos) in stream: 
     510            # for each repeat tag found 
     511            if mark is ENTER: 
     512                # get the number of columns for that table 
     513                attrs = data[1] 
     514                table = attrs.get('table') 
     515                col_count = self.counter.counters[table] 
     516 
     517                # collect events (column header tags) to repeat 
     518                events = [] 
     519                for submark, event in stream: 
     520                    if submark is EXIT: 
     521                        break 
     522                    events.append(event) 
     523 
     524                # repeat them 
     525                for _ in range(col_count): 
     526                    for event in events: 
     527                        yield None, event 
     528            else: 
     529                yield mark, (kind, data, pos) 
    397530 
    398531