def test_table_with_colalign(self): """Table columns can be right justified""" # First with default alignment actual_result = Table(['12', '3000', '5']) expected_strings = ['<td colspan="100%">12</td>', '<td colspan="100%">3000</td>', '<td colspan="100%">5</td>'] for s in expected_strings: message = ('Did not find expected string "%s" in result: %s' % (s, actual_result)) assert s in str(actual_result).strip(), message # Then using explicit alignment (all right justified) # FIXME (Ole): This does not work if e.g. col_align has # different strings: col_align = ['right', 'left', 'center'] actual_result = Table(['12', '3000', '5'], col_align=['right', 'right', 'right']) expected_strings = [ ('<td colspan="100%" align="right" style="text-align: ' 'right;">12</td>'), ('<td colspan="100%" align="right" style="text-align: ' 'right;">3000</td>'), ('<td colspan="100%" align="right" style="text-align: ' 'right;">5</td>')] for s in expected_strings: message = ('Did not find expected string "%s" in result: %s' % (s, actual_result)) assert s in str(actual_result).strip(), message # Now try at the TableRow level # FIXME (Ole): Breaks tables! # row = TableRow(['12', '3000', '5'], # col_align=['right', 'right', 'right']) # actual_result = Table(row) # print actual_result # This breaks too - what's going on? # row = TableRow(['12', '3000', '5']) # actual_result = Table(row) # print actual_result # Try at the cell level cell_1 = TableCell('12', align='right') cell_2 = TableCell('3000', align='right') cell_3 = TableCell('5', align='right') row = TableRow([cell_1, cell_2, cell_3]) # print row # OK # This is OK for cell in [cell_1, cell_2, cell_3]: msg = 'Wrong cell alignment %s' % cell assert 'align="right"' in str(cell), msg table = Table(row) self.html += str(table) self.writeHtml('table_column_alignment')
def test_nested_table_in_cell(self): """Test nested table in cell""" inner_table = Table([['1', '2', '3']]) cell = TableCell(inner_table) self.html += ' <h2>Table nested in Cell</h2>\n' body = (' <tbody>\n' ' <tr>\n' ' <td><table class="table table-striped condensed">\n' ' <tbody>\n' ' <tr>\n' ' <td>1</td>\n' ' <td>2</td>\n' ' <td>3</td>\n' ' </tr>\n' ' </tbody>\n' '</table></td>\n' ' </tr>\n' ' </tbody>\n') expected_result = ('%s%s%s' % (self.html_table_start, body, self.html_table_end)) actual_result = Table([[cell]]) message = 'Expected: %s\n\nGot: %s' % (expected_result, actual_result) self.html += str(actual_result) self.writeHtml('table_nested_in_cell') assert expected_result.strip() == str(actual_result).strip(), message
def test_row_span(self): """Testing row spanning""" table_cell_aa = TableCell('aa spanned', row_span=2) table_row1 = TableRow([ table_cell_aa, self.table_cell_b, self.table_cell_c, self.table_cell_d ]) table_row2 = TableRow( [self.table_cell_b, self.table_cell_c, self.table_cell_d]) self.html += ' <h2>Spanning Table Columns</h2>\n' body = (' <tbody>\n' ' <tr>\n' ' <td rowspan="2">aa spanned</td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' <tr>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' </tbody>\n') expected_result = ('%s%s%s' % (self.html_table_start, body, self.html_table_end)) actual_result = Table([table_row1, table_row2]) message = 'Expected: %s\n\nGot: %s' % (expected_result, actual_result) assert expected_result.strip() == str(actual_result).strip(), message self.html += str(actual_result) self.writeHtml('table_rowspanning')
def get_plugins_as_table(name=None): """Retrieve a table listing all plugins and their requirements. Or just a single plugin if name is passed. Args: name str optional name of a specific plugin. Returns: table instance containing plugin descriptive data Raises: None """ table_body = [] header = TableRow([_('Title'), _('ID'), _('Requirements')], header=True) table_body.append(header) plugins_dict = dict([(pretty_function_name(p), p) for p in FunctionProvider.plugins]) if name is not None: if isinstance(name, basestring): # Add the names plugins_dict.update( dict([(p.__name__, p) for p in FunctionProvider.plugins])) msg = ('No plugin named "%s" was found. ' 'List of available plugins is: %s' % (name, ', '.join(plugins_dict.keys()))) if name not in plugins_dict: raise RuntimeError(msg) plugins_dict = {name: plugins_dict[name]} else: msg = ('get_plugins expects either no parameters or a string ' 'with the name of the plugin, you passed: ' '%s which is a %s' % (name, type(name))) raise Exception(msg) # Now loop through the plugins adding them to the table for key, func in plugins_dict.iteritems(): for requirement in requirements_collect(func): row = [] row.append(TableCell(get_function_title(func), header=True)) row.append(key) row.append(requirement) table_body.append(TableRow(row)) table = Table(table_body) table.caption = _('Available Impact Functions') return table
def test_cell_link(self): """Test cell links work""" table_cell_link = Link('InaSAFE', 'http://inasafe.org') table_row = TableRow([ TableCell(table_cell_link), self.table_cell_b, self.table_cell_c, self.table_cell_d ]) self.html += ' <h2>Link Cell Columns</h2>\n' body = (' <tbody>\n' ' <tr>\n' ' <td><a href="http://inasafe.org">InaSAFE</a></td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' </tbody>\n') expected_result = ('%s%s%s' % (self.html_table_start, body, self.html_table_end)) actual_result = Table([table_row]) message = 'Expected: %s\n\nGot: %s' % (expected_result, actual_result) assert expected_result.strip() == str(actual_result).strip(), message self.html += str(actual_result) self.writeHtml('table_colspanning')
def get_plugins_as_table(dict_filter=None): """Retrieve a table listing all plugins and their requirements. Or just a single plugin if name is passed. Args: * dict_filter = dictionary that contains filters - id = list_id - title = list_title - category : list_category - subcategory : list_subcategory - layertype : list_layertype - datatype : list_datatype - unit: list_unit - disabled : list_disabled # not included Returns: * table contains plugins match with dict_filter Raises: None """ if dict_filter is None: dict_filter = { 'id': [], 'title': [], 'category': [], 'subcategory': [], 'layertype': [], 'datatype': [], 'unit': [] } table_body = [] # use this list for avoiding wrong order in dict atts = ['category', 'subcategory', 'layertype', 'datatype', 'unit'] header = TableRow([ tr('Title'), tr('ID'), tr('Category'), tr('Sub Category'), tr('Layer type'), tr('Data type'), tr('Unit') ], header=True) table_body.append(header) plugins_dict = dict([(pretty_function_name(p), p) for p in FunctionProvider.plugins]) not_found_value = 'N/A' for key, func in plugins_dict.iteritems(): for requirement in requirements_collect(func): dict_found = { 'title': False, 'id': False, 'category': False, 'subcategory': False, 'layertype': False, 'datatype': False, 'unit': False } dict_req = parse_single_requirement(str(requirement)) # If the impact function is disabled, do not show it if dict_req.get('disabled', False): continue for myKey in dict_found.iterkeys(): myFilter = dict_filter.get(myKey, []) if myKey == 'title': myValue = str(get_function_title(func)) elif myKey == 'id': myValue = str(key) else: myValue = dict_req.get(myKey, not_found_value) if myFilter != []: for myKeyword in myFilter: if type(myValue) == type(str()): if myValue == myKeyword: dict_found[myKey] = True break elif type(myValue) == type(list()): if myKeyword in myValue: dict_found[myKey] = True break else: if myValue.find(str(myKeyword)) != -1: dict_found[myKey] = True break else: dict_found[myKey] = True add_row = True for found_value in dict_found.itervalues(): if not found_value: add_row = False break if add_row: row = [] row.append(TableCell(get_function_title(func), header=True)) row.append(key) for myKey in atts: myValue = pretty_string( dict_req.get(myKey, not_found_value)) row.append(myValue) table_body.append(TableRow(row)) cw = 100 / 7 table_col_width = [ str(cw) + '%', str(cw) + '%', str(cw) + '%', str(cw) + '%', str(cw) + '%', str(cw) + '%', str(cw) + '%' ] table = Table(table_body, col_width=table_col_width) table.caption = tr('Available Impact Functions') return table
def run(self, layers): """Risk plugin for flood population evacuation. :param layers: List of layers expected to contain * hazard_layer : Vector polygon layer of flood depth * exposure_layer : Raster layer of population data on the same grid as hazard_layer Counts number of people exposed to areas identified as flood prone :returns: Map of population exposed to flooding Table with number of people evacuated and supplies required. :rtype: tuple """ # Identify hazard and exposure layers hazard_layer = get_hazard_layer(layers) # Flood inundation exposure_layer = get_exposure_layer(layers) question = get_question(hazard_layer.get_name(), exposure_layer.get_name(), self) # Check that hazard is polygon type if not hazard_layer.is_vector: message = ('Input hazard %s was not a vector layer as expected ' % hazard_layer.get_name()) raise Exception(message) message = ( 'Input hazard must be a polygon layer. I got %s with layer type ' '%s' % (hazard_layer.get_name(), hazard_layer.get_geometry_name())) if not hazard_layer.is_polygon_data: raise Exception(message) # Run interpolation function for polygon2raster P = assign_hazard_values_to_exposure_data(hazard_layer, exposure_layer, attribute_name='population') # Initialise attributes of output dataset with all attributes # from input polygon and a population count of zero new_attributes = hazard_layer.get_data() category_title = 'affected' # FIXME: Should come from keywords deprecated_category_title = 'FLOODPRONE' categories = {} for attr in new_attributes: attr[self.target_field] = 0 try: cat = attr[category_title] except KeyError: try: cat = attr['FLOODPRONE'] categories[cat] = 0 except KeyError: pass # Count affected population per polygon, per category and total affected_population = 0 for attr in P.get_data(): affected = False if 'affected' in attr: res = attr['affected'] if res is None: x = False else: x = bool(res) affected = x elif 'FLOODPRONE' in attr: # If there isn't an 'affected' attribute, res = attr['FLOODPRONE'] if res is not None: affected = res.lower() == 'yes' elif 'Affected' in attr: # Check the default attribute assigned for points # covered by a polygon res = attr['Affected'] if res is None: x = False else: x = res affected = x else: # assume that every polygon is affected (see #816) affected = True # there is no flood related attribute # message = ('No flood related attribute found in %s. ' # 'I was looking for either "Flooded", "FLOODPRONE" ' # 'or "Affected". The latter should have been ' # 'automatically set by call to ' # 'assign_hazard_values_to_exposure_data(). ' # 'Sorry I can\'t help more.') # raise Exception(message) if affected: # Get population at this location pop = float(attr['population']) # Update population count for associated polygon poly_id = attr['polygon_id'] new_attributes[poly_id][self.target_field] += pop # Update population count for each category if len(categories) > 0: try: cat = new_attributes[poly_id][category_title] except KeyError: cat = new_attributes[poly_id][ deprecated_category_title] categories[cat] += pop # Update total affected_population += pop # Estimate number of people in need of evacuation evacuated = (affected_population * self.parameters['evacuation_percentage'] / 100.0) affected_population, rounding = population_rounding_full( affected_population) total = int(numpy.sum(exposure_layer.get_data(nan=0, scaling=False))) # Don't show digits less than a 1000 total = population_rounding(total) evacuated, rounding_evacuated = population_rounding_full(evacuated) minimum_needs = [ parameter.serialize() for parameter in self.parameters['minimum needs'] ] # Generate impact report for the pdf map table_body = [ question, TableRow([ tr('People affected'), '%s*' % (format_int(int(affected_population))) ], header=True), TableRow([ TableCell(tr('* Number is rounded up to the nearest %s') % (rounding), col_span=2) ]), TableRow([ tr('People needing evacuation'), '%s*' % (format_int(int(evacuated))) ], header=True), TableRow([ TableCell(tr('* Number is rounded up to the nearest %s') % (rounding_evacuated), col_span=2) ]), TableRow([ tr('Evacuation threshold'), '%s%%' % format_int(self.parameters['evacuation_percentage']) ], header=True), TableRow( tr('Map shows the number of people affected in each flood prone ' 'area')), TableRow( tr('Table below shows the weekly minimum needs for all ' 'evacuated people')) ] total_needs = evacuated_population_needs(evacuated, minimum_needs) for frequency, needs in total_needs.items(): table_body.append( TableRow([ tr('Needs should be provided %s' % frequency), tr('Total') ], header=True)) for resource in needs: table_body.append( TableRow([ tr(resource['table name']), format_int(resource['amount']) ])) impact_table = Table(table_body).toNewlineFreeString() table_body.append(TableRow(tr('Action Checklist:'), header=True)) table_body.append(TableRow(tr('How will warnings be disseminated?'))) table_body.append(TableRow(tr('How will we reach stranded people?'))) table_body.append(TableRow(tr('Do we have enough relief items?'))) table_body.append( TableRow( tr('If yes, where are they located and how will we distribute ' 'them?'))) table_body.append( TableRow( tr('If no, where can we obtain additional relief items from and ' 'how will we transport them to here?'))) # Extend impact report for on-screen display table_body.extend([ TableRow(tr('Notes'), header=True), tr('Total population: %s') % format_int(total), tr('People need evacuation if in the area identified as ' '"Flood Prone"'), tr('Minimum needs are defined in BNPB regulation 7/2008') ]) impact_summary = Table(table_body).toNewlineFreeString() # Create style # Define classes for legend for flooded population counts colours = [ '#FFFFFF', '#38A800', '#79C900', '#CEED00', '#FFCC00', '#FF6600', '#FF0000', '#7A0000' ] population_counts = [x['population'] for x in new_attributes] classes = create_classes(population_counts, len(colours)) interval_classes = humanize_class(classes) # Define style info for output polygons showing population counts style_classes = [] for i in xrange(len(colours)): style_class = dict() style_class['label'] = create_label(interval_classes[i]) if i == 0: transparency = 0 style_class['min'] = 0 else: transparency = 0 style_class['min'] = classes[i - 1] style_class['transparency'] = transparency style_class['colour'] = colours[i] style_class['max'] = classes[i] style_classes.append(style_class) # Override style info with new classes and name style_info = dict(target_field=self.target_field, style_classes=style_classes, style_type='graduatedSymbol') # For printing map purpose map_title = tr('People affected by flood prone areas') legend_notes = tr('Thousand separator is represented by \'.\'') legend_units = tr('(people per polygon)') legend_title = tr('Population Count') # Create vector layer and return vector_layer = Vector(data=new_attributes, projection=hazard_layer.get_projection(), geometry=hazard_layer.get_geometry(), name=tr('People affected by flood prone areas'), keywords={ 'impact_summary': impact_summary, 'impact_table': impact_table, 'target_field': self.target_field, 'map_title': map_title, 'legend_notes': legend_notes, 'legend_units': legend_units, 'legend_title': legend_title, 'affected_population': affected_population, 'total_population': total, 'total_needs': total_needs }, style_info=style_info) return vector_layer
def run(self, layers): """Risk plugin for flood population evacuation Input: layers: List of layers expected to contain my_hazard : Vector polygon layer of flood depth my_exposure : Raster layer of population data on the same grid as my_hazard Counts number of people exposed to areas identified as flood prone Return Map of population exposed to flooding Table with number of people evacuated and supplies required """ # Identify hazard and exposure layers my_hazard = get_hazard_layer(layers) # Flood inundation my_exposure = get_exposure_layer(layers) question = get_question(my_hazard.get_name(), my_exposure.get_name(), self) # Check that hazard is polygon type if not my_hazard.is_vector: msg = ('Input hazard %s was not a vector layer as expected ' % my_hazard.get_name()) raise Exception(msg) msg = ('Input hazard must be a polygon layer. I got %s with layer ' 'type %s' % (my_hazard.get_name(), my_hazard.get_geometry_name())) if not my_hazard.is_polygon_data: raise Exception(msg) # Run interpolation function for polygon2raster P = assign_hazard_values_to_exposure_data(my_hazard, my_exposure, attribute_name='population') # Initialise attributes of output dataset with all attributes # from input polygon and a population count of zero new_attributes = my_hazard.get_data() category_title = 'affected' # FIXME: Should come from keywords deprecated_category_title = 'FLOODPRONE' categories = {} for attr in new_attributes: attr[self.target_field] = 0 try: cat = attr[category_title] except KeyError: cat = attr['FLOODPRONE'] categories[cat] = 0 # Count affected population per polygon, per category and total affected_population = 0 for attr in P.get_data(): affected = False if 'affected' in attr: res = attr['affected'] if res is None: x = False else: x = bool(res) affected = x elif 'FLOODPRONE' in attr: # If there isn't an 'affected' attribute, res = attr['FLOODPRONE'] if res is not None: affected = res.lower() == 'yes' elif 'Affected' in attr: # Check the default attribute assigned for points # covered by a polygon res = attr['Affected'] if res is None: x = False else: x = res affected = x else: # there is no flood related attribute msg = ('No flood related attribute found in %s. ' 'I was looking fore either "Flooded", "FLOODPRONE" ' 'or "Affected". The latter should have been ' 'automatically set by call to ' 'assign_hazard_values_to_exposure_data(). ' 'Sorry I can\'t help more.') raise Exception(msg) if affected: # Get population at this location pop = float(attr['population']) # Update population count for associated polygon poly_id = attr['polygon_id'] new_attributes[poly_id][self.target_field] += pop # Update population count for each category try: cat = new_attributes[poly_id][category_title] except KeyError: cat = new_attributes[poly_id][deprecated_category_title] categories[cat] += pop # Update total affected_population += pop affected_population = round_thousand(affected_population) # Estimate number of people in need of evacuation evacuated = (affected_population * self.parameters['evacuation_percentage'] / 100.0) total = int(numpy.sum(my_exposure.get_data(nan=0, scaling=False))) # Don't show digits less than a 1000 total = round_thousand(total) evacuated = round_thousand(evacuated) # Calculate estimated minimum needs minimum_needs = self.parameters['minimum needs'] tot_needs = evacuated_population_weekly_needs(evacuated, minimum_needs) # Generate impact report for the pdf map table_body = [ question, TableRow([ tr('People affected'), '%s%s' % (format_int(int(affected_population)), ('*' if affected_population >= 1000 else '')) ], header=True), TableRow([ tr('People needing evacuation'), '%s%s' % (format_int(int(evacuated)), ('*' if evacuated >= 1000 else '')) ], header=True), TableRow([ TableCell(tr('* Number is rounded to the nearest 1000'), col_span=2) ], header=False), TableRow([ tr('Evacuation threshold'), '%s%%' % format_int(self.parameters['evacuation_percentage']) ], header=True), TableRow( tr('Map shows population affected in each flood' ' prone area')), TableRow( tr('Table below shows the weekly minium needs ' 'for all evacuated people')), TableRow([tr('Needs per week'), tr('Total')], header=True), [tr('Rice [kg]'), format_int(tot_needs['rice'])], [ tr('Drinking Water [l]'), format_int(tot_needs['drinking_water']) ], [tr('Clean Water [l]'), format_int(tot_needs['water'])], [tr('Family Kits'), format_int(tot_needs['family_kits'])], [tr('Toilets'), format_int(tot_needs['toilets'])] ] impact_table = Table(table_body).toNewlineFreeString() table_body.append(TableRow(tr('Action Checklist:'), header=True)) table_body.append(TableRow(tr('How will warnings be disseminated?'))) table_body.append(TableRow(tr('How will we reach stranded people?'))) table_body.append(TableRow(tr('Do we have enough relief items?'))) table_body.append( TableRow( tr('If yes, where are they located and how ' 'will we distribute them?'))) table_body.append( TableRow( tr('If no, where can we obtain additional ' 'relief items from and how will we ' 'transport them to here?'))) # Extend impact report for on-screen display table_body.extend([ TableRow(tr('Notes'), header=True), tr('Total population: %s') % format_int(total), tr('People need evacuation if in area identified ' 'as "Flood Prone"'), tr('Minimum needs are defined in BNPB ' 'regulation 7/2008') ]) impact_summary = Table(table_body).toNewlineFreeString() # Create style # Define classes for legend for flooded population counts colours = [ '#FFFFFF', '#38A800', '#79C900', '#CEED00', '#FFCC00', '#FF6600', '#FF0000', '#7A0000' ] population_counts = [x['population'] for x in new_attributes] classes = create_classes(population_counts, len(colours)) interval_classes = humanize_class(classes) # Define style info for output polygons showing population counts style_classes = [] for i in xrange(len(colours)): style_class = dict() style_class['label'] = create_label(interval_classes[i]) if i == 0: transparency = 100 style_class['min'] = 0 else: transparency = 0 style_class['min'] = classes[i - 1] style_class['transparency'] = transparency style_class['colour'] = colours[i] style_class['max'] = classes[i] style_classes.append(style_class) # Override style info with new classes and name style_info = dict(target_field=self.target_field, style_classes=style_classes, style_type='graduatedSymbol') # For printing map purpose map_title = tr('People affected by flood prone areas') legend_notes = tr('Thousand separator is represented by \'.\'') legend_units = tr('(people per polygon)') legend_title = tr('Population Count') # Create vector layer and return V = Vector(data=new_attributes, projection=my_hazard.get_projection(), geometry=my_hazard.get_geometry(), name=tr('Population affected by flood prone areas'), keywords={ 'impact_summary': impact_summary, 'impact_table': impact_table, 'target_field': self.target_field, 'map_title': map_title, 'legend_notes': legend_notes, 'legend_units': legend_units, 'legend_title': legend_title }, style_info=style_info) return V
def _tabulate(self, affected_population, evacuated, minimum_needs, question, rounding, rounding_evacuated): # People Affected table_body = [ question, TableRow([ tr('People affected'), '%s*' % (format_int(int(affected_population))) ], header=True) ] if self.use_affected_field: table_body.append( TableRow( tr('* People are considered to be affected if they are ' 'within the area where the value of the hazard field (' '"%s") is "%s"') % (self.parameters['affected_field'], self.parameters['affected_value']))) else: table_body.append( TableRow( tr('* People are considered to be affected if they are ' 'within any polygons.'))) table_body.append( TableRow([ TableCell(tr('* Number is rounded up to the nearest %s') % rounding, col_span=2) ])) # People Needing Evacuation table_body.append( TableRow([ tr('People needing evacuation'), '%s*' % (format_int(int(evacuated))) ], header=True)) table_body.append( TableRow([ TableCell(tr('* Number is rounded up to the nearest %s') % rounding_evacuated, col_span=2) ])) table_body.append( TableRow([ tr('Evacuation threshold'), '%s%%' % format_int(self.parameters['evacuation_percentage']) ], header=True)) table_body.append( TableRow( tr('Table below shows the weekly minimum needs for all ' 'evacuated people'))) total_needs = evacuated_population_needs(evacuated, minimum_needs) for frequency, needs in total_needs.items(): table_body.append( TableRow([ tr('Needs should be provided %s' % frequency), tr('Total') ], header=True)) for resource in needs: table_body.append( TableRow([ tr(resource['table name']), format_int(resource['amount']) ])) return table_body, total_needs
def setUp(self): """Fixture run before all tests""" self.table_header = ['1', '2', '3', '4'] self.table_data = [['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']] self.table_row = TableRow(['a', 'b', 'c', 'd']) self.table_row_data = [ self.table_row, self.table_row, self.table_row, self.table_row ] self.table_cell_a = TableCell('a') self.table_cell_b = TableCell('b') self.table_cell_c = TableCell('c') self.table_cell_d = TableCell('d') self.table_row_cells = TableRow([ self.table_cell_a, self.table_cell_b, self.table_cell_c, self.table_cell_d ]) self.table_cell_data = [ self.table_row_cells, self.table_row_cells, self.table_row_cells, self.table_row_cells ] self.table_caption = 'Man this is a nice table!' self.html_table_start = ('<table class="table table-striped' ' condensed">\n') self.html_table_end = '</table>\n' self.html_caption = ' <caption>Man this is a nice table!</caption>\n' self.html_bottom_caption = (' <caption class="caption-bottom">' 'Man this is a nice table!</caption>\n') self.html_header = (' <thead>\n' ' <tr>\n' ' <th>1</th>\n' ' <th>2</th>\n' ' <th>3</th>\n' ' <th>4</th>\n' ' </tr>\n' ' </thead>\n') self.html_body = (' <tbody>\n' ' <tr>\n' ' <td>a</td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' <tr>\n' ' <td>a</td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' <tr>\n' ' <td>a</td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' <tr>\n' ' <td>a</td>\n' ' <td>b</td>\n' ' <td>c</td>\n' ' <td>d</td>\n' ' </tr>\n' ' </tbody>\n')
def test_cell_header(self): """Test we can make a cell as a <th> element""" cell = TableCell('Foo', header=True) row = TableRow([cell]) table = Table(row) del table