fix: Excel-Tabellenfarben in Importvorschau darstellen
This commit is contained in:
@@ -5,7 +5,7 @@ class DocumentationConfig(PluginConfig):
|
|||||||
name = "netbox_documentation"
|
name = "netbox_documentation"
|
||||||
verbose_name = "NetBox Dokumentation"
|
verbose_name = "NetBox Dokumentation"
|
||||||
description = "Wiki und Office-Dokumentation direkt in NetBox"
|
description = "Wiki und Office-Dokumentation direkt in NetBox"
|
||||||
version = "0.7.6"
|
version = "0.7.7"
|
||||||
author = "LKE"
|
author = "LKE"
|
||||||
base_url = "documentation"
|
base_url = "documentation"
|
||||||
min_version = "4.0.0"
|
min_version = "4.0.0"
|
||||||
|
|||||||
@@ -204,8 +204,14 @@ def _excel_color(color, workbook):
|
|||||||
|
|
||||||
def _excel_cell_style(cell, workbook):
|
def _excel_cell_style(cell, workbook):
|
||||||
declarations = []
|
declarations = []
|
||||||
if cell.fill and cell.fill.fill_type == "solid":
|
if cell.fill and cell.fill.fill_type:
|
||||||
fill = _excel_color(cell.fill.fgColor, workbook)
|
fill = (
|
||||||
|
_excel_color(getattr(cell.fill, "fgColor", None), workbook) or
|
||||||
|
_excel_color(getattr(cell.fill, "bgColor", None), workbook)
|
||||||
|
)
|
||||||
|
if not fill and cell.fill.fill_type == "linear":
|
||||||
|
stops = getattr(cell.fill, "stop", ())
|
||||||
|
fill = _excel_color(stops[0].color, workbook) if stops else None
|
||||||
if fill:
|
if fill:
|
||||||
declarations.append(f"background-color: {fill}")
|
declarations.append(f"background-color: {fill}")
|
||||||
font_color = _excel_color(cell.font.color, workbook)
|
font_color = _excel_color(cell.font.color, workbook)
|
||||||
@@ -226,6 +232,52 @@ def _excel_cell_style(cell, workbook):
|
|||||||
return "; ".join(declarations)
|
return "; ".join(declarations)
|
||||||
|
|
||||||
|
|
||||||
|
def _tint_hex(value, tint):
|
||||||
|
import colorsys
|
||||||
|
rgb = tuple(int(value[index:index + 2], 16) / 255 for index in (1, 3, 5))
|
||||||
|
hue, lightness, saturation = colorsys.rgb_to_hls(*rgb)
|
||||||
|
lightness = lightness * (1 + tint) if tint < 0 else lightness + (1 - lightness) * tint
|
||||||
|
tinted = colorsys.hls_to_rgb(hue, max(0, min(1, lightness)), saturation)
|
||||||
|
return "#" + "".join(f"{round(component * 255):02x}" for component in tinted)
|
||||||
|
|
||||||
|
|
||||||
|
def _excel_table_cell_styles(sheet, workbook, max_row, max_column):
|
||||||
|
"""Create visual fallbacks for Excel's built-in 'Format as Table' styles."""
|
||||||
|
from openpyxl.styles import Color
|
||||||
|
from openpyxl.utils.cell import range_boundaries
|
||||||
|
|
||||||
|
styles = {}
|
||||||
|
for table in sheet.tables.values():
|
||||||
|
info = table.tableStyleInfo
|
||||||
|
match = re.fullmatch(r"TableStyle(Light|Medium|Dark)(\d+)", info.name or "") if info else None
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
family, number = match.group(1), int(match.group(2))
|
||||||
|
palette_slot = (number - 1) % 7
|
||||||
|
theme_index = 0 if palette_slot == 0 else 3 + palette_slot
|
||||||
|
base = _excel_color(Color(theme=theme_index), workbook) or "#4472c4"
|
||||||
|
min_col, min_row, table_max_col, table_max_row = range_boundaries(table.ref)
|
||||||
|
table_max_col, table_max_row = min(table_max_col, max_column), min(table_max_row, max_row)
|
||||||
|
if min_col > table_max_col or min_row > table_max_row:
|
||||||
|
continue
|
||||||
|
header_fill = _tint_hex(base, 0.55) if family == "Light" else base
|
||||||
|
header_color = "#000000" if family == "Light" else "#ffffff"
|
||||||
|
stripe_fill = _tint_hex(base, 0.88 if family == "Light" else 0.82)
|
||||||
|
for column in range(min_col, table_max_col + 1):
|
||||||
|
styles[(min_row, column)] = f"background-color: {header_fill}; color: {header_color}; font-weight: bold"
|
||||||
|
data_start = min_row + (1 if table.headerRowCount else 0)
|
||||||
|
data_end = table_max_row - (1 if table.totalsRowShown else 0)
|
||||||
|
if info.showRowStripes:
|
||||||
|
for row in range(data_start, data_end + 1):
|
||||||
|
if (row - data_start) % 2 == 1:
|
||||||
|
for column in range(min_col, table_max_col + 1):
|
||||||
|
styles[(row, column)] = f"background-color: {stripe_fill}"
|
||||||
|
if table.totalsRowShown and table_max_row >= min_row:
|
||||||
|
for column in range(min_col, table_max_col + 1):
|
||||||
|
styles[(table_max_row, column)] = f"font-weight: bold; border-top: 2px solid {base}"
|
||||||
|
return styles
|
||||||
|
|
||||||
|
|
||||||
def _render_excel_worksheet_html(sheet, workbook, max_rows=None):
|
def _render_excel_worksheet_html(sheet, workbook, max_rows=None):
|
||||||
"""Render worksheet values and the most relevant visual Excel cell formatting."""
|
"""Render worksheet values and the most relevant visual Excel cell formatting."""
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
@@ -240,6 +292,7 @@ def _render_excel_worksheet_html(sheet, workbook, max_rows=None):
|
|||||||
return "<p><em>Keine Inhalte</em></p>"
|
return "<p><em>Keine Inhalte</em></p>"
|
||||||
max_row = min(last_row, max_rows) if max_rows else last_row
|
max_row = min(last_row, max_rows) if max_rows else last_row
|
||||||
max_column = last_column
|
max_column = last_column
|
||||||
|
table_styles = _excel_table_cell_styles(sheet, workbook, max_row, max_column)
|
||||||
merged_starts, merged_children = {}, set()
|
merged_starts, merged_children = {}, set()
|
||||||
for merged in sheet.merged_cells.ranges:
|
for merged in sheet.merged_cells.ranges:
|
||||||
if merged.min_row > max_row or merged.min_col > max_column:
|
if merged.min_row > max_row or merged.min_col > max_column:
|
||||||
@@ -273,6 +326,10 @@ def _render_excel_worksheet_html(sheet, workbook, max_rows=None):
|
|||||||
if colspan > 1:
|
if colspan > 1:
|
||||||
attributes.append(f'colspan="{colspan}"')
|
attributes.append(f'colspan="{colspan}"')
|
||||||
style = _excel_cell_style(cell, workbook)
|
style = _excel_cell_style(cell, workbook)
|
||||||
|
if not style and (row, column) in table_styles:
|
||||||
|
style = table_styles[(row, column)]
|
||||||
|
elif (row, column) in table_styles and "background-color" not in style:
|
||||||
|
style = "; ".join(filter(None, (style, table_styles[(row, column)])))
|
||||||
if style:
|
if style:
|
||||||
attributes.append(f'style="{style}"')
|
attributes.append(f'style="{style}"')
|
||||||
value = "" if cell.value is None else escape(str(cell.value)).replace("\n", "<br>")
|
value = "" if cell.value is None else escape(str(cell.value)).replace("\n", "<br>")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
.excel-preview-content { max-height: 32rem; overflow: auto; }
|
.excel-preview-content { max-height: 32rem; overflow: auto; }
|
||||||
.excel-preview-content table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: .825rem; }
|
.excel-preview-content table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: .825rem; }
|
||||||
.excel-preview-content th, .excel-preview-content td { min-width: 7rem; max-width: 22rem; padding: .3rem .45rem; border: 1px solid var(--tblr-border-color, #adb5bd); vertical-align: top; overflow-wrap: anywhere; }
|
.excel-preview-content th, .excel-preview-content td { min-width: 7rem; max-width: 22rem; padding: .3rem .45rem; border: 1px solid var(--tblr-border-color, #adb5bd); vertical-align: top; overflow-wrap: anywhere; }
|
||||||
.excel-preview-content th { background: var(--tblr-bg-surface-secondary, #f1f3f5); }
|
.excel-preview-content th:not([style*="background"]) { background: var(--tblr-bg-surface-secondary, #f1f3f5); }
|
||||||
</style>
|
</style>
|
||||||
{% endblock head %}
|
{% endblock head %}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-documentation"
|
name = "netbox-documentation"
|
||||||
version = "0.7.6"
|
version = "0.7.7"
|
||||||
description = "Integrated Markdown wiki and office document importer for NetBox"
|
description = "Integrated Markdown wiki and office document importer for NetBox"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
+24
-1
@@ -6,7 +6,7 @@ import sys
|
|||||||
import pytest
|
import pytest
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.styles import PatternFill, Font
|
from openpyxl.styles import PatternFill, Font
|
||||||
from openpyxl.worksheet.table import Table
|
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||||
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
spec = importlib.util.spec_from_file_location(
|
||||||
@@ -65,6 +65,29 @@ def test_xlsx_preserves_cell_fill_and_font_colors_as_html():
|
|||||||
assert "background-color: #eaf0dc" in result.markdown
|
assert "background-color: #eaf0dc" in result.markdown
|
||||||
|
|
||||||
|
|
||||||
|
def test_xlsx_renders_builtin_excel_table_style_colors_in_preview_html():
|
||||||
|
workbook = Workbook()
|
||||||
|
sheet = workbook.active
|
||||||
|
sheet.append(["Standort", "Adresse"])
|
||||||
|
sheet.append(["Verwaltung", "10.1.2.17"])
|
||||||
|
sheet.append(["Büro", "10.1.2.18"])
|
||||||
|
table = Table(displayName="Standorte", ref="A1:B3")
|
||||||
|
table.tableStyleInfo = TableStyleInfo(
|
||||||
|
name="TableStyleMedium4", showFirstColumn=False,
|
||||||
|
showLastColumn=False, showRowStripes=True, showColumnStripes=False,
|
||||||
|
)
|
||||||
|
sheet.add_table(table)
|
||||||
|
stream = BytesIO()
|
||||||
|
workbook.save(stream)
|
||||||
|
|
||||||
|
result = import_document(Upload(stream.getvalue(), "tabelle.xlsx"))
|
||||||
|
|
||||||
|
assert result.body_format == "html"
|
||||||
|
assert "Standort" in result.markdown
|
||||||
|
assert result.markdown.count("background-color:") >= 4
|
||||||
|
assert "color: #ffffff" in result.markdown
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_legacy_excel():
|
def test_rejects_legacy_excel():
|
||||||
with pytest.raises(ImportFailure, match="xlsx"):
|
with pytest.raises(ImportFailure, match="xlsx"):
|
||||||
import_document(Upload(b"", "legacy.xls"))
|
import_document(Upload(b"", "legacy.xls"))
|
||||||
|
|||||||
Reference in New Issue
Block a user