############################################################################## # 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 Element: def __init__( self, elt: str, content=None, classes: list[str] = None, attrs: dict[str, str] = None, data: dict = None, ): self.elt = elt self.attrs = attrs or {} self.classes = classes or [] "list of classes for the element" self.content = content self.data = data or {} "data-xxx" def html(self, extra_classes: set[str] = None) -> str: "html for element" classes = [cls for cls in (self.classes + (list(extra_classes or []))) if cls] attrs_str = f"""class="{' '.join(classes)}" """ if classes else "" # 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()]) return f"""<{self.elt} {attrs_str}>{self.html_content()}{self.elt}>""" def html_content(self) -> str: "Le contenu de l'élément, en html." return str(self.content or "") def add_class(self, klass: str): "Add a class, do nothing if already there" if klass not in self.classes: self.classes.append(klass) class Table(Element): """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.insert_group(group:str, [after=str], [before=str]) Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion On fixe l'ordre des groupes par ordre d'insertion ou par insert_group ou par set_column_groups. """ def __init__( self, selected_row_id: str = None, classes: list[str] = None, attrs: dict[str, str] = None, data: dict = None, ): super().__init__("table", classes=classes, attrs=attrs, data=data) self.rows: list["Row"] = [] "ordered list of Rows" self.row_by_id: dict[str, "Row"] = {} self.column_ids = [] "ordered list of columns ids" 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_classes: defaultdict[str, set[str]] = defaultdict(set) "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 _prepare(self): """Prepare the table before generation: Sort table columns, add header/footer titles rows """ self.sort_columns() # Titres self.add_head_row(self.head_title_row) self.add_foot_row(self.foot_title_row) def get_row_by_id(self, row_id) -> "Row": "return the row, or None" return self.row_by_id.get(row_id) def is_empty(self) -> bool: "true if table has no rows" return len(self.rows) == 0 def select_row(self, row_id): "mark rows as 'selected'" self.selected_row_id = row_id def to_list(self) -> list[dict]: """as a list, each row is a dict""" self._prepare() return [row.to_dict() for row in self.rows] def html(self, extra_classes: list[str] = None) -> str: """HTML version of the table""" self._prepare() return super().html(extra_classes=extra_classes) def html_content(self) -> str: """Le contenu de la table en html.""" 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)} cols_order = {col_id: i for i, col_id in enumerate(self.column_ids)} self.column_ids.sort( key=lambda col_id: ( groups_order.get(self.column_group.get(col_id), col_id), cols_order[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, classes: list[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 col_id not in self.titles: self.titles[col_id] = title self.head_title_row.cells[col_id] = self.head_title_row.add_cell( col_id, None, title, classes=classes, group=self.column_group.get(col_id), ) self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( col_id, None, title, classes=classes ) return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id] class Row(Element): """A row.""" def __init__( self, table: Table, row_id=None, category=None, cell_elt: str = None, classes: list[str] = None, attrs: dict[str, str] = None, data: dict = None, ): super().__init__("tr", classes=classes, attrs=attrs, data=data) self.category = category self.cells = {} self.cell_elt = cell_elt self.classes: list[str] = classes or [] "classes sur le