From 342bff3b56cc86295837e8499d9a97678b54365f Mon Sep 17 00:00:00 2001 From: jmpla Date: Mon, 15 May 2023 06:32:54 +0200 Subject: [PATCH] =?UTF-8?q?formattage=20termin=C3=A9=20=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/prepajury_cells.py | 42 +-- app/but/prepajury_desc.py | 40 ++- app/but/prepajury_xl.py | 127 +-------- app/but/prepajury_xl_format.py | 476 +++++++++++++++++++++++++++++++++ tests/api/make_samples.py | 5 - 5 files changed, 549 insertions(+), 141 deletions(-) create mode 100644 app/but/prepajury_xl_format.py diff --git a/app/but/prepajury_cells.py b/app/but/prepajury_cells.py index 86124152a..aa08fdd3b 100644 --- a/app/but/prepajury_cells.py +++ b/app/but/prepajury_cells.py @@ -1,26 +1,36 @@ -from enum import Enum -from app.but.prepajury_xl import ScoExcelSheet, excel_make_style +from app.but.prepajury_xl import ( + ScoExcelSheet, +) +from app.but.prepajury_xl_format import ( + SCO_HALIGN, + SCO_VALIGN, + SCO_FONTNAME, + SCO_FONTSIZE, + FMT, + Sco_Style, +) -base_style = excel_make_style() +base_signature = ( + FMT.FONT_NAME.write(SCO_FONTNAME.FONTNAME_CALIBRI) + + FMT.FONT_SIZE.write(SCO_FONTSIZE.FONTSIZE_10) + + FMT.ALIGNMENT_HALIGN.write(SCO_HALIGN.HALIGN_CENTER) + + FMT.ALIGNEMENT_VALIGN.write(SCO_VALIGN.VALIGN_CENTER) +) -class BG(Enum): - - NONE = 0, - BLACK = 1, - GREY = 2, - BUT1 class Cell: - def __init__(self, text: str = None, style: int = None, comment: str = None): + def __init__( + self, text: str = None, signature: int = base_signature, comment: str = None + ): self.text = text - self.style = style or base_style + self.signature = signature or base_signature self.comment = comment - @staticmethod - def get_style(): - return base_style + def format(self, FMT: FMT, value=None): + self.signature = FMT.composante.write(self.signature, value) def make_cell(self, worksheet: ScoExcelSheet): - style = self.get_style() - cell = worksheet.make_cell(self.text or "", style=style) + cell = worksheet.make_cell(self.text or "") + style: Sco_Style = FMT.ALL.get_style(self.signature) + style.apply(cell) return cell diff --git a/app/but/prepajury_desc.py b/app/but/prepajury_desc.py index 9914914b5..ec7c4c72c 100644 --- a/app/but/prepajury_desc.py +++ b/app/but/prepajury_desc.py @@ -1,5 +1,8 @@ +from openpyxl.styles.colors import BLACK + from app.but.prepajury_cells import Cell, base_style -from app.but.prepajury_xl import excel_make_style +from app.but.prepajury_xl import Sco_Style +from app.but.prepajury_xl_format import SCO_COLORS from app.models import ApcCompetence, ApcParcours from app.scodoc.sco_excel import ScoExcelBook, ScoExcelSheet @@ -9,6 +12,23 @@ def parite(semestre_idx): listeAnnees = ["BUT1", "BUT2", "BUT3"] +header_colors = { + "BUT1": { + "BUT": SCO_COLORS.BUT1, + "RCUE": SCO_COLORS.RCUE1, + "UE": SCO_COLORS.UE1, + }, + "BUT2": { + "BUT": SCO_COLORS.BUT2, + "RCUE": SCO_COLORS.RCUE2, + "UE": SCO_COLORS.UE2, + }, + "BUT3": { + "BUT": SCO_COLORS.BUT3, + "RCUE": SCO_COLORS.RCUE3, + "UE": SCO_COLORS.UE3, + }, +} class _Header: @@ -20,7 +40,7 @@ class _Header: def setLabelOnce(self, rang: int, label: str): self.presets[rang] = label - def setStyles(self, rangs: list[int], style): + def setStyles(self, rangs: list[int], style: Sco_Style): for rang in rangs: self.styles[rang] = style @@ -71,10 +91,16 @@ class NiveauDesc: self.ue[parite(scodocUe.semestre_idx)] = scodocUe def generate_header(self, header): + rcue_style = Sco_Style( + fromStyle=base_style, + bgcolor=header_colors[self.fromScodoc.annee]["RCUE"], + ) + ue_style = Sco_Style( + fromStyle=base_style, + bgcolor=header_colors[self.fromScodoc.annee]["UE"], + ) header.setLabelOnce(1, self.fromScodoc.competence.titre) - rcue_style = excel_make_style(base_style) - ue_style = excel_make_style(base_style) - header.setStyles([1, 2, 3], rcue_style) + header.setStyles([1], rcue_style) header.setStyles([2, 3], ue_style) for ue in self.ue: if ue is None: @@ -135,7 +161,9 @@ class AnneeDesc: header.add_column() def generate_header(self, header): - but_style = excel_make_style(base_style) + but_style = Sco_Style( + fromStyle=base_style, bgcolor=header_colors[self.codeAnnee]["BUT"] + ) header.setStyles([0], but_style) for niveau in self.niveaux.values(): niveau.generate_header(header) diff --git a/app/but/prepajury_xl.py b/app/but/prepajury_xl.py index 7890f65c6..7a1c7e50e 100644 --- a/app/but/prepajury_xl.py +++ b/app/but/prepajury_xl.py @@ -25,38 +25,25 @@ # ############################################################################## +from __future__ import annotations + +from openpyxl.cell import WriteOnlyCell + +from app.but.prepajury_xl_format import Sco_Style """ Excel file handling """ import datetime -import io -import time -from enum import Enum from tempfile import NamedTemporaryFile import openpyxl.utils.datetime -from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY +from openpyxl.styles.numbers import FORMAT_DATE_DDMMYY from openpyxl.comments import Comment -from openpyxl import Workbook, load_workbook -from openpyxl.cell import WriteOnlyCell -from openpyxl.styles import Font, Border, Side, Alignment, PatternFill -from openpyxl.worksheet.worksheet import Worksheet +from openpyxl import Workbook + import app.scodoc.sco_utils as scu -from app import log from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc import notesdb, sco_preferences - - -class COLORS(Enum): - BLACK = "FF000000" - WHITE = "FFFFFFFF" - RED = "FFFF0000" - BROWN = "FF993300" - PURPLE = "FF993366" - BLUE = "FF0000FF" - ORANGE = "FFFF3300" - LIGHT_YELLOW = "FFFFFF99" # Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante: @@ -127,67 +114,6 @@ class ScoExcelBook: return tmp.read() -def excel_make_style( - fromStyle=None, - bold=False, - italic=False, - outline=False, - color: COLORS = COLORS.BLACK, - bgcolor: COLORS = None, - halign=None, - valign=None, - number_format=None, - font_name="Arial", - size=10, -): - """Contruit un style. - Les couleurs peuvent être spécfiées soit par une valeur de COLORS, - soit par une chaine argb (exple "FF00FF00" pour le vert) - color -- La couleur du texte - bgcolor -- la couleur de fond - halign -- alignement horizontal ("left", "right", "center") - valign -- alignement vertical ("top", "bottom", "center") - number_format -- formattage du contenu ("General", "@", ...) - font_name -- police - size -- taille de police - """ - style = {} - if fromStyle is not None: - for item in fromStyle: - style[item] = fromStyle[item] - font = Font( - name=font_name, - bold=bold, - italic=italic, - outline=outline, - color=color.value, - size=size, - ) - style["font"] = font - if bgcolor: - style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) - if halign or valign: - al = Alignment() - if halign: - al.horizontal = { - "left": "left", - "right": "right", - "center": "center", - }[halign] - if valign: - al.vertical = { - "top": "top", - "bottom": "bottom", - "center": "center", - }[valign] - style["alignment"] = al - if number_format is None: - style["number_format"] = FORMAT_GENERAL - else: - style["number_format"] = number_format - return style - - class ScoExcelSheet: """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook. En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations @@ -208,9 +134,7 @@ class ScoExcelSheet: # Le nom de la feuille ne peut faire plus de 31 caractères. # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) self.sheet_name = adjust_sheetname(sheet_name) - if default_style is None: - default_style = excel_make_style() - self.default_style = default_style + self.default_style = default_style or Sco_Style() if wb is None: self.wb = Workbook() self.ws = self.wb.active @@ -223,29 +147,6 @@ class ScoExcelSheet: self.column_dimensions = {} self.row_dimensions = {} - def excel_make_composite_style( - self, - alignment=None, - border=None, - fill=None, - number_format=None, - font=None, - ): - style = {} - if font is not None: - style["font"] = font - if alignment is not None: - style["alignment"] = alignment - if border is not None: - style["border"] = border - if fill is not None: - style["fill"] = fill - if number_format is None: - style["number_format"] = FORMAT_GENERAL - else: - style["number_format"] = number_format - return style - @staticmethod def i2col(idx): if idx < 26: # one letter key @@ -287,7 +188,7 @@ class ScoExcelSheet: """ self.ws.row_dimensions[cle].hidden = value - def make_cell(self, value: any = None, style=None, comment=None): + def make_cell(self, value: any = None, style: Sco_Style = None, comment=None): """Construit une cellule. value -- contenu de la cellule (texte, numérique, booléen ou date) style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié @@ -313,17 +214,15 @@ class ScoExcelSheet: if "font" in style: cell.font = style["font"] if "alignment" in style: - cell.alignment = style["alignment"] + cell.alignment = style["alignment"].get_openxl() if "border" in style: - cell.border = style["border"] - if "fill" in style: - cell.fill = style["fill"] + cell.border = style["border"].get_openxl() if "number_format" in style: cell.number_format = style["number_format"] if "fill" in style: cell.fill = style["fill"] if "alignment" in style: - cell.alignment = style["alignment"] + cell.alignment = style["alignment"].get_openxl() if not comment is None: cell.comment = Comment(comment, "scodoc") lines = comment.splitlines() diff --git a/app/but/prepajury_xl_format.py b/app/but/prepajury_xl_format.py new file mode 100644 index 000000000..660d9106d --- /dev/null +++ b/app/but/prepajury_xl_format.py @@ -0,0 +1,476 @@ +import abc +from enum import Enum + +import openpyxl.styles +from openpyxl.styles import Side, Border, Font, PatternFill +from openpyxl.styles.numbers import FORMAT_GENERAL, FORMAT_NUMBER_00 + + +class SCO_COLORS(Enum): + def __new__(cls, value, argb): + obj = object.__new__(cls) + obj._value_ = value + obj.argb = argb + return obj + + BLACK = (1, "FF000000") + WHITE = (2, "FFFFFFFF") + RED = (3, "FFFF0000") + BROWN = (4, "FF993300") + PURPLE = (5, "FF993366") + BLUE = (6, "FF0000FF") + ORANGE = (7, "FFFF3300") + LIGHT_YELLOW = (8, "FFFFFF99") + BUT1 = (9, "FF95B3D7") + RCUE1 = (10, "FFB8CCE4") + UE1 = (11, "FFDCE6F1") + BUT2 = (12, "FFC4D79B") + RCUE2 = (13, "FFD8E4BC") + UE2 = (14, "FFEBF1DE") + BUT3 = (15, "FFFABF8F") + RCUE3 = (16, "FFFCD5B4") + UE3 = (17, "FFFDE9D9") + + +class SCO_BORDERTHICKNESS(Enum): + def __new__(cls, value, width): + obj = object.__new__(cls) + obj._value_ = value + obj.width = width + return obj + + BORDER_NONE = (1, None) + BORDER_HAIR = (2, "hair") + BORDER_THIN = (3, "thin") + BORDER_MEDIUM = (4, "medium") + BORDER_THICK = (5, "thick") + + +class SCO_FONTNAME(Enum): + def __new__(cls, value, fontname): + obj = object.__new__(cls) + obj._value_ = value + obj.fontname = fontname + return obj + + FONTNAME_ARIAL = (1, "Arial") + FONTNAME_COURIER = (2, "Courier New") + FONTNAME_CALIBRI = (3, "Calibri") + FONTNAME_TIMES = (4, "Times New Roman") + + +class SCO_FONTSIZE(Enum): + def __new__(cls, value, fontsize): + obj = object.__new__(cls) + obj._value_ = value + obj.fontsize = fontsize + return obj + + FONTSIZE_9 = (1, 9) + FONTSIZE_10 = (1, 10) + FONTSIZE_11 = (1, 11) + FONTSIZE_13 = (1, 13) + FONTSIZE_16 = (1, 16) + + +class SCO_NUMBER_FORMAT(Enum): + def __new__(cls, value, format): + obj = object.__new__(cls) + obj._value_ = value + obj.format = format + return obj + + NUMBER_GENERAL = (0, FORMAT_GENERAL) + NUMBER_00 = (1, FORMAT_NUMBER_00) + + +class SCO_HALIGN(Enum): + def __new__(cls, value, position): + obj = object.__new__(cls) + obj._value_ = value + obj.position = position + return obj + + HALIGN_CENTER = (1, "center") + HALIGN_RIGHT = (2, "right") + HALIGN_LEFT = (3, "left") + + +class SCO_VALIGN(Enum): + def __new__(cls, value, position): + obj = object.__new__(cls) + obj._value_ = value + obj.position = position + return obj + + VALIGN_TOP = (1, "top") + VALIGN_BOTTOM = (2, "bottom") + VALIGN_CENTER = (3, "center") + + +free = 0 + + +class Composante: + def __init__(self, base=None, width: int = 1): + global free + self.cache = {} + if base is None: + self.base = free + free += width + else: + self.base = base + self.width = width + self.end = self.base + self.width + self.mask = ((1 << width) - 1) << self.base + + def read(self, signature: int) -> int: + return (signature & self.mask) >> self.base + + def clear(self, signature: int) -> int: + return signature & ~self.mask + + def write(self, index, signature=0) -> int: + return self.clear(signature) + index << self.base + + def lookup_or_cache(self, signature: int): + value = self.read(signature) + if not value in self.cache: + self.cache[signature] = self.build(value) + return self.cache[value] + + @abc.abstractmethod + def build(self, value: int): + pass + + +class Composante_boolean(Composante): + def __init__(self): + super().__init__(width=1) + + def build(self, signature): + value = self.read(signature) + return value == 1 + + +class Composante_number_format(Composante): + def __init__(self): + super().__init__(width=2) + + def build(self, signature: int): + value = self.read(signature) + return SCO_NUMBER_FORMAT(value) or None + + +class Composante_Colors(Composante): + def __init__(self): + super().__init__(width=5) + + def build(self, signature: int): + value = self.read(signature) + return SCO_COLORS(value) or SCO_COLORS.BLACK + + +class Composante_borderThickness(Composante): + def __init__(self): + super().__init__(width=2) + + def build(self, signature: int): + value = self.read(signature) + return SCO_BORDERTHICKNESS(value) or None + + +class Composante_fontname(Composante): + def __init__(self): + super().__init__(width=3) + + def build(self, signature: int): + value = self.read(signature) + return SCO_FONTNAME(value) or None + + +class Composante_fontsize(Composante): + def __init__(self): + super().__init__(width=3) + + def build(self, signature: int): + value = self.read(signature) + return SCO_FONTSIZE(value) or None + + +class Composante_halign(Composante): + def __init__(self): + super().__init__(width=2) + + def build(self, signature: int): + value = self.read(signature) + return SCO_HALIGN(value) or None + + +class Composante_valign(Composante): + def __init__(self): + super().__init__(width=2) + + def build(self, signature: int): + value = self.read(signature) + return SCO_VALIGN(signature) or None + + +class Sco_BorderSide: + def __init__( + self, + thickness: SCO_BORDERTHICKNESS = None, + color: SCO_COLORS = SCO_COLORS.WHITE, + ): + self.thickness = thickness + self.color: SCO_COLORS = color + + def get_openxl(self): + side: Side = Side(border_style=self.thickness.width, color=self.color.argb) + return side + + +class Sco_Borders: + def __init__( + self, + left: Sco_BorderSide = None, + right: Sco_BorderSide = None, + top: Sco_BorderSide = None, + bottom: Sco_BorderSide = None, + ): + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + def get_openxl(self): + border: Border = Border( + left=self.left.get_openxl(), + right=self.right.get_openxl(), + top=self.top.get_openxl(), + bottom=self.bottom.get_openxl(), + ) + + +class Sco_Alignment: + def __init__( + self, + halign: SCO_HALIGN = None, + valign: SCO_VALIGN = None, + ): + self.halign = halign + self.valign = valign + + def get_openxl(self): + al = openpyxl.styles.Alignment() + al.horizontal = self.halign.position + al.vertical = self.valign.position + return al + + +class Sco_Font: + def __init__( + self, + name: str = None, + fontsize: int = None, + bold: bool = None, + italic: bool = None, + outline: bool = None, + color: "SCO_COLORS" = None, + ): + self.name = name + self.bold = bold + self.italic = italic + self.outline = outline + self.color = color + self.fontsize = fontsize + + +class Sco_Style: + from openpyxl.cell import WriteOnlyCell, Cell + + def __init__( + self, + font: Sco_Font = None, + bgcolor: SCO_COLORS = None, + alignment: Sco_Alignment = None, + borders: Sco_Borders = None, + number_format: SCO_NUMBER_FORMAT = None, + ): + self.font = font + self.bgcolor = bgcolor + self.alignment = alignment + self.borders = borders + self.number_format = number_format + + def apply(self, cell: Cell): + if self.font: + cell.font = self.font.get_openxl() + if self.bgcolor: + cell.fill = PatternFill(fill_type="solid", fgColor=self.bgcolor.argb) + if self.alignment: + cell.alignment = self.alignment.get_openxl() + if self.borders: + cell.border = self.borders.get_openxl() + if self.number_format: + cell.number_format = self.number_format.get_openxl() + + +class Composante_group(Composante): + def __init__(self, composantes: list[Composante]): + self.composantes = composantes + mini = min([comp.base for comp in composantes]) + maxi = max([comp.end for comp in composantes]) + width = sum([comp.width for comp in composantes]) + if not width == (maxi - mini): + raise Exception("Composante group non complete ou non connexe") + super().__init__(base=mini, width=width) + + +class Composante_font(Composante_group): + def __init__( + self, + name: Composante_fontname, + fontsize: Composante_fontsize, + color: Composante_Colors, + bold: Composante_boolean, + italic: Composante_boolean, + outline: Composante_boolean, + ): + super().__init__([name, fontsize, color, bold, italic, outline]) + self.name = name + self.fontsize = fontsize + self.color = color + self.bold = bold + self.italic = italic + self.outline = outline + + def build(self, signature): + return Sco_Font( + name=self.name.lookup_or_cache(signature), + fontsize=self.fontsize.lookup_or_cache(signature), + color=self.color.lookup_or_cache(signature), + bold=self.bold.lookup_or_cache(signature), + italic=self.italic.lookup_or_cache(signature), + outline=self.outline.lookup_or_cache(signature), + ) + + +class Composante_border(Composante_group): + def __init__(self, thick: Composante_borderThickness, color: Composante_Colors): + super().__init__([thick, color]) + self.thick = thick + self.color = color + + def build(self, signature: int): + return Sco_BorderSide( + thickness=self.thick.lookup_or_cache(signature), + color=self.color.lookup_or_cache(signature), + ) + + +class Composante_borders(Composante_group): + def __init__( + self, + left: Composante_border, + right: Composante_border, + top: Composante_border, + bottom: Composante_border, + ): + super().__init__([left, right, top, bottom]) + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + def build(self, signature: int): + return Sco_Borders( + left=self.left.lookup_or_cache(signature), + right=self.right.lookup_or_cache(signature), + top=self.top.lookup_or_cache(signature), + bottom=self.bottom.lookup_or_cache(signature), + ) + + +class Composante_alignment(Composante_group): + def __init__(self, halign: Composante_halign, valign: Composante_valign): + super().__init__([halign, valign]) + self.halign = halign + self.valign = valign + + def build(self, signature: int): + return Sco_Alignment( + halign=self.halign.lookup_or_cache(signature), + valign=self.valign.lookup_or_cache(signature), + ) + + # ALL = Composante_all(FONT, BGCOLOR, BORDERS, ALIGNMENT, NUMBER_FORMAT) + + +class Composante_all(Composante_group): + def __init__( + self, + font: Composante_font, + bgcolor: Composante_Colors, + borders: Composante_borders, + alignment: Composante_alignment, + number_format: Composante_number_format, + ): + super().__init__([font, bgcolor, borders, alignment, number_format]) + self.font = font + self.bgcolor = bgcolor + self.borders = borders + self.alignment = alignment + self.number_format = number_format + + def build(self, signature: int): + return Sco_Style( + bgcolor=self.bgcolor.lookup_or_cache(signature), + font=self.font.lookup_or_cache(signature), + borders=self.borders.lookup_or_cache(signature), + alignment=self.alignment.lookup_or_cache(signature), + number_format=self.number_format.lookup_or_cache(signature), + ) + + def get_style(self, signature: int): + return self.lookup_or_cache(signature) + + +class FMT(Enum): + def __init__(self, composante: Composante): + self.composante = composante + + def write(self, value, signature=0) -> int: + return self.composante.write(value, signature) + + def get_style(self, signature: int): + return self.composante.lookup_or_cache(signature) + + FONT_NAME = Composante_fontname() + FONT_SIZE = Composante_fontsize() + FONT_COLOR = Composante_Colors() + FONT_BOLD = Composante_boolean() + FONT_ITALIC = Composante_boolean() + FONT_OUTLINE = Composante_boolean() + BORDER_LEFT_STYLE = Composante_borderThickness() + BORDER_LEFT_COLOR = Composante_Colors() + BORDER_RIGHT_STYLE = Composante_borderThickness() + BORDER_RIGHT_COLOR = Composante_Colors() + BORDER_TOP_STYLE = Composante_borderThickness() + BORDER_TOP_COLOR = Composante_Colors() + BORDER_BOTTOM_STYLE = Composante_borderThickness() + BORDER_BOTTOM_COLOR = Composante_Colors() + BGCOLOR = Composante_Colors() + ALIGNMENT_HALIGN = Composante_halign() + ALIGNEMENT_VALIGN = Composante_valign() + NUMBER_FORMAT = Composante_number_format() + FONT = Composante_font( + FONT_NAME, FONT_SIZE, FONT_COLOR, FONT_BOLD, FONT_ITALIC, FONT_OUTLINE + ) + BORDER_LEFT = Composante_border(BORDER_LEFT_STYLE, BORDER_LEFT_COLOR) + BORDER_RIGHT = Composante_border(BORDER_RIGHT_STYLE, BORDER_RIGHT_COLOR) + BORDER_TOP = Composante_border(BORDER_TOP_STYLE, BORDER_TOP_COLOR) + BORDER_BOTTOM = Composante_border(BORDER_BOTTOM_STYLE, BORDER_BOTTOM_COLOR) + BORDERS = Composante_borders(BORDER_LEFT, BORDER_RIGHT, BORDER_TOP, BORDER_BOTTOM) + ALIGNMENT = Composante_alignment(ALIGNMENT_HALIGN, ALIGNMENT_HALIGN) + ALL = Composante_all(FONT, BGCOLOR, BORDERS, ALIGNMENT, NUMBER_FORMAT) diff --git a/tests/api/make_samples.py b/tests/api/make_samples.py index fd61346f9..41689ee74 100644 --- a/tests/api/make_samples.py +++ b/tests/api/make_samples.py @@ -53,15 +53,10 @@ import json from pandas import read_csv from setup_test_api import ( - API_PASSWORD, - API_URL, - API_USER, - APIError, CHECK_CERTIFICATE, get_auth_headers, GET, POST_JSON, - SCODOC_URL, ) DATA_DIR = "/tmp/samples/"