############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Classes pour aider à construire des tables de résultats """ from collections import defaultdict class Table: """Construction d'une table de résultats table = Table() row = table.new_row(id="xxx", category="yyy") row.new_cell( col_id, title, content [,classes] [, idx], [group], [keys:dict={}] ) rows = table.get_rows([category="yyy"]) table.sort_rows(key [, reverse]) table.set_titles(titles) table.update_titles(titles) table.set_column_groups(groups: list[str]) table.sort_columns() table.insert_group(group:str, [after=str], [before=str]) """ def __init__( self, selected_row_id: str = None, classes: list[str] = None, data: dict[str, str] = None, ): self.rows: list["Row"] = [] "ordered list of Rows" self.row_by_id: dict[str, "Row"] = {} self.classes = classes or [] "list of classes for the table element" self.column_ids = [] "ordered list of columns ids" self.data = data or {} "data-xxx" self.groups = [] "ordered list of column groups names" self.head = [] self.foot = [] self.column_group = {} "the group of the column: { col_id : group }" self.column_index: dict[str, int] = {} "index the column: { col_id : int }" self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: []) "classe ajoutée à toutes les cellules de la colonne: { col_id : class }" self.selected_row_id = selected_row_id "l'id de la ligne sélectionnée" self.titles = {} "Column title: { col_id : titre }" self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th") self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th") self.empty_cell = Cell.empty() def get_row_by_id(self, row_id) -> "Row": "return the row, or None" return self.row_by_id.get(row_id) def _prepare(self): """Sort table elements before generation""" self.sort_columns() def html(self, classes: list[str] = None, data: dict[str, str] = None) -> str: """HTML version of the table classes are prepended to existing classes data may replace existing data """ self._prepare() classes = classes + self.classes data = self.data.copy().update(data) elt_class = f"""class="{' '.join(classes)}" """ if classes else "" attrs_str = " ".join(f' data-{k}="v"' for k, v in data.items()) newline = "\n" header = ( f""" { newline.join(row.html() for row in self.head) } """ if self.head else "" ) footer = ( f""" { newline.join(row.html() for row in self.foot) } """ if self.foot else "" ) return f""" {header} { newline.join(row.html() for row in self.rows) } {footer}
""" def add_row(self, row: "Row") -> "Row": """Append a new row""" self.rows.append(row) self.row_by_id[row.id] = row return row def add_head_row(self, row: "Row") -> "Row": "Add a row to table head" # row = Row(self, cell_elt="th", category="head") self.head.append(row) self.row_by_id[row.id] = row return row def add_foot_row(self, row: "Row") -> "Row": "Add a row to table foot" self.foot.append(row) self.row_by_id[row.id] = row return row def sort_rows(self, key: callable, reverse: bool = False): """Sort table rows""" self.rows.sort(key=key, reverse=reverse) def sort_columns(self): """Sort columns ids""" groups_order = {group: i for i, group in enumerate(self.groups)} self.column_ids.sort( key=lambda col_id: (groups_order.get(self.column_group.get(col_id), col_id)) ) def insert_group(self, group: str, after: str = None, before: str = None): """Déclare un groupe de colonnes et le place avant ou après un autre groupe. Si pas d'autre groupe indiqué, le place après, à droite du dernier. Si le group existe déjà, ne fait rien (ne le déplace pas). """ if group in self.groups: return other = after or before if other is None: self.groups.append(group) else: if not other in self.groups: raise ValueError(f"invalid column group '{other}'") index = self.groups.index(other) if after: index += 1 self.groups.insert(index, group) def set_groups(self, groups: list[str]): """Define column groups and set order""" self.groups = groups def set_titles(self, titles: dict[str, str]): """Set columns titles""" self.titles = titles def update_titles(self, titles: dict[str, str]): """Set columns titles""" self.titles.update(titles) def add_title(self, col_id, title: str = None) -> tuple["Cell", "Cell"]: """Record this title, and create cells for footer and header if they don't already exist. """ title = title or "" if not col_id in self.titles: self.titles[col_id] = title cell_head = self.head_title_row.cells.get( col_id, self.head_title_row.add_cell(col_id, title, title) ) cell_foot = self.foot_title_row.cells.get( col_id, self.foot_title_row.add_cell(col_id, title, title) ) return cell_head, cell_foot class Row: """A row.""" def __init__( self, table: Table, row_id=None, category=None, cell_elt: str = None, classes: list[str] = None, ): self.category = category self.cells = {} self.cell_elt = cell_elt self.classes: list[str] = classes or [] "classes sur le " self.cur_idx_by_group = defaultdict(lambda: 0) self.id = row_id self.table = table def add_cell( self, col_id: str, title: str, content: str, group: str = None, attrs: list[str] = None, classes: list[str] = None, data: dict[str, str] = None, elt: str = None, idx: int = None, raw_content=None, target_attrs: dict = None, target: str = None, ) -> "Cell": """Create cell and add it to the row. classes is a list of css class names """ if idx is None: idx = self.cur_idx_by_group[group] self.cur_idx_by_group[group] += 1 else: self.cur_idx_by_group[group] = idx self.table.column_index[col_id] = idx cell = Cell( content, classes + [group or ""], # ajoute le nom de groupe aux classes elt=elt or self.cell_elt, attrs=attrs, data=data, raw_content=raw_content, target=target, target_attrs=target_attrs, ) return self.add_cell_instance(col_id, cell, column_group=group, title=title) def add_cell_instance( self, col_id: str, cell: "Cell", column_group: str = None, title: str = None ) -> "Cell": """Add a cell to the row. Si title est None, il doit avoir été ajouté avec table.add_title(). """ self.cells[col_id] = cell if column_group is not None: self.table.column_group[col_id] = column_group if title is not None: self.table.add_title(col_id, title) return cell def html(self) -> str: """html for row, with cells""" elt_class = f"""class="{' '.join(self.classes)}" """ if self.classes else "" tr_id = ( """id="row_selected" """ if (self.id == self.table.selected_etudid) else "" ) # TODO XXX remplacer id par une classe return f"""{ "".join([self.cells.get(col_id, self.table.empty_cell).html( column_classes=self.table.column_classes.get(col_id) ) for col_id in self.table.column_ids ]) }""" class BottomRow(Row): """Une ligne spéciale pour le pied de table avec un titre à gauche (répété sur les colonnes indiquées par left_title_col_ids), et automatiquement ajouté au footer. """ def __init__( self, *args, left_title_col_ids: list[str] = None, left_title=None, **kwargs ): super().__init__(*args, **kwargs) self.left_title_col_ids = left_title_col_ids if left_title is not None: self.set_left_title(left_title) self.table.add_foot_row(self) def set_left_title(self, title: str = ""): "Fill left title cells" for col_id in self.left_title_col_ids: self.add_cell(col_id, None, title) class Cell: """Une cellule de table""" def __init__( self, content, classes: list[str] = None, elt="td", attrs: list[str] = None, data: dict = None, raw_content=None, target: str = None, target_attrs: dict = None, ): """if specified, raw_content will be used for raw exports like xlsx""" self.content = content self.classes: list = classes or [] self.elt = elt if elt is not None else "td" self.attrs = attrs or [] if self.elt == "th": self.attrs["scope"] = "row" self.data = data or {} self.raw_content = raw_content # not yet used self.target = target self.target_attrs = target_attrs or {} @classmethod def empty(cls): "create a new empty cell" return cls("") def __str__(self): return str(self.content) def html(self, column_classes: list[str] = None) -> str: "html for cell" attrs_str = f"""class="{' '.join( [cls for cls in (self.classes + (column_classes or [])) if cls]) }" """ # Autres attributs: attrs_str += " ".join([f"{k}={v}" for (k, v) in self.attrs.items()]) # et data-x attrs_str += " ".join([f' data-{k}="v"' for k, v in self.data.items()]) if (self.target is not None) or self.target_attrs: href = f'href="{self.target}"' if self.target else "" target_attrs_str = " ".join( [f"{k}={v}" for (k, v) in self.target_attrs.items()] ) content = f"{content}" return f"""<{self.elt} {attrs_str}>{self.content}"""