- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py - Uses openpyxl exclusively to preserve Excel formatting and formulas - Updated server.js to use new xlsxwriter scripts for form submissions - Maintains all original functionality while ensuring proper Excel file handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
393 lines
11 KiB
Python
393 lines
11 KiB
Python
###############################################################################
|
|
#
|
|
# Comments - A class for writing the Excel XLSX Worksheet file.
|
|
#
|
|
# SPDX-License-Identifier: BSD-2-Clause
|
|
#
|
|
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
|
#
|
|
|
|
from typing import Dict, List, Optional, Union
|
|
|
|
from xlsxwriter.color import Color
|
|
|
|
from . import xmlwriter
|
|
from .utility import _preserve_whitespace, xl_cell_to_rowcol, xl_rowcol_to_cell
|
|
|
|
|
|
###########################################################################
|
|
#
|
|
# A comment type class.
|
|
#
|
|
###########################################################################
|
|
class CommentType:
|
|
"""
|
|
A class to represent a comment in an Excel worksheet.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
row: int,
|
|
col: int,
|
|
text: str,
|
|
options: Optional[Dict[str, Union[str, int, float]]] = None,
|
|
) -> None:
|
|
"""
|
|
Initialize a Comment instance.
|
|
|
|
Args:
|
|
row (int): The row number of the comment.
|
|
col (int): The column number of the comment.
|
|
text (str): The text of the comment.
|
|
options (dict): Additional options for the comment.
|
|
"""
|
|
self.row: int = row
|
|
self.col: int = col
|
|
self.text: str = text
|
|
|
|
self.author: Optional[str] = None
|
|
self.color: Color = Color("#ffffe1")
|
|
|
|
self.start_row: int = 0
|
|
self.start_col: int = 0
|
|
|
|
self.is_visible: Optional[bool] = None
|
|
|
|
self.width: float = 128
|
|
self.height: float = 74
|
|
|
|
self.x_scale: float = 1
|
|
self.y_scale: float = 1
|
|
self.x_offset: int = 0
|
|
self.y_offset: int = 0
|
|
|
|
self.font_size: float = 8
|
|
self.font_name: str = "Tahoma"
|
|
self.font_family: int = 2
|
|
|
|
self.vertices: List[Union[int, float]] = []
|
|
|
|
# Set the default start cell and offsets for the comment.
|
|
self.set_offsets(self.row, self.col)
|
|
|
|
# Set any user supplied options.
|
|
self._set_user_options(options)
|
|
|
|
def _set_user_options(
|
|
self, options: Optional[Dict[str, Union[str, int, float]]] = None
|
|
) -> None:
|
|
"""
|
|
This method handles the additional optional parameters to
|
|
``write_comment()``.
|
|
"""
|
|
if options is None:
|
|
return
|
|
|
|
# Overwrite the defaults with any user supplied values. Incorrect or
|
|
# misspelled parameters are silently ignored.
|
|
width = options.get("width")
|
|
if width and isinstance(width, (int, float)):
|
|
self.width = width
|
|
|
|
height = options.get("height")
|
|
if height and isinstance(height, (int, float)):
|
|
self.height = height
|
|
|
|
x_offset = options.get("x_offset")
|
|
if x_offset and isinstance(x_offset, int):
|
|
self.x_offset = x_offset
|
|
|
|
y_offset = options.get("y_offset")
|
|
if y_offset and isinstance(y_offset, int):
|
|
self.y_offset = y_offset
|
|
|
|
start_col = options.get("start_col")
|
|
if start_col and isinstance(start_col, int):
|
|
self.start_col = start_col
|
|
|
|
start_row = options.get("start_row")
|
|
if start_row and isinstance(start_row, int):
|
|
self.start_row = start_row
|
|
|
|
font_size = options.get("font_size")
|
|
if font_size and isinstance(font_size, (int, float)):
|
|
self.font_size = font_size
|
|
|
|
font_name = options.get("font_name")
|
|
if font_name and isinstance(font_name, str):
|
|
self.font_name = font_name
|
|
|
|
font_family = options.get("font_family")
|
|
if font_family and isinstance(font_family, int):
|
|
self.font_family = font_family
|
|
|
|
author = options.get("author")
|
|
if author and isinstance(author, str):
|
|
self.author = author
|
|
|
|
visible = options.get("visible")
|
|
if visible is not None and isinstance(visible, bool):
|
|
self.is_visible = visible
|
|
|
|
if options.get("color"):
|
|
# Set the comment background color.
|
|
self.color = Color._from_value(options["color"])
|
|
|
|
# Convert a cell reference to a row and column.
|
|
start_cell = options.get("start_cell")
|
|
if start_cell and isinstance(start_cell, str):
|
|
(start_row, start_col) = xl_cell_to_rowcol(start_cell)
|
|
self.start_row = start_row
|
|
self.start_col = start_col
|
|
|
|
# Scale the size of the comment box if required.
|
|
x_scale = options.get("x_scale")
|
|
if x_scale and isinstance(x_scale, (int, float)):
|
|
self.width = self.width * x_scale
|
|
|
|
y_scale = options.get("y_scale")
|
|
if y_scale and isinstance(y_scale, (int, float)):
|
|
self.height = self.height * y_scale
|
|
|
|
# Round the dimensions to the nearest pixel.
|
|
self.width = int(0.5 + self.width)
|
|
self.height = int(0.5 + self.height)
|
|
|
|
def set_offsets(self, row: int, col: int) -> None:
|
|
"""
|
|
Set the default start cell and offsets for the comment. These are
|
|
generally a fixed offset relative to the parent cell. However there are
|
|
some edge cases for cells at the, well, edges.
|
|
"""
|
|
row_max = 1048576
|
|
col_max = 16384
|
|
|
|
if self.row == 0:
|
|
self.y_offset = 2
|
|
self.start_row = 0
|
|
elif self.row == row_max - 3:
|
|
self.y_offset = 16
|
|
self.start_row = row_max - 7
|
|
elif self.row == row_max - 2:
|
|
self.y_offset = 16
|
|
self.start_row = row_max - 6
|
|
elif self.row == row_max - 1:
|
|
self.y_offset = 14
|
|
self.start_row = row_max - 5
|
|
else:
|
|
self.y_offset = 10
|
|
self.start_row = row - 1
|
|
|
|
if self.col == col_max - 3:
|
|
self.x_offset = 49
|
|
self.start_col = col_max - 6
|
|
elif self.col == col_max - 2:
|
|
self.x_offset = 49
|
|
self.start_col = col_max - 5
|
|
elif self.col == col_max - 1:
|
|
self.x_offset = 49
|
|
self.start_col = col_max - 4
|
|
else:
|
|
self.x_offset = 15
|
|
self.start_col = col + 1
|
|
|
|
|
|
###########################################################################
|
|
#
|
|
# The file writer class for the Excel XLSX Comments file.
|
|
#
|
|
###########################################################################
|
|
class Comments(xmlwriter.XMLwriter):
|
|
"""
|
|
A class for writing the Excel XLSX Comments file.
|
|
|
|
|
|
"""
|
|
|
|
###########################################################################
|
|
#
|
|
# Public API.
|
|
#
|
|
###########################################################################
|
|
|
|
def __init__(self) -> None:
|
|
"""
|
|
Constructor.
|
|
|
|
"""
|
|
|
|
super().__init__()
|
|
self.author_ids = {}
|
|
|
|
###########################################################################
|
|
#
|
|
# Private API.
|
|
#
|
|
###########################################################################
|
|
|
|
def _assemble_xml_file(
|
|
self, comments_data: Optional[List[CommentType]] = None
|
|
) -> None:
|
|
# Assemble and write the XML file.
|
|
|
|
if comments_data is None:
|
|
comments_data = []
|
|
|
|
# Write the XML declaration.
|
|
self._xml_declaration()
|
|
|
|
# Write the comments element.
|
|
self._write_comments()
|
|
|
|
# Write the authors element.
|
|
self._write_authors(comments_data)
|
|
|
|
# Write the commentList element.
|
|
self._write_comment_list(comments_data)
|
|
|
|
self._xml_end_tag("comments")
|
|
|
|
# Close the file.
|
|
self._xml_close()
|
|
|
|
###########################################################################
|
|
#
|
|
# XML methods.
|
|
#
|
|
###########################################################################
|
|
|
|
def _write_comments(self) -> None:
|
|
# Write the <comments> element.
|
|
xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
|
|
attributes = [("xmlns", xmlns)]
|
|
|
|
self._xml_start_tag("comments", attributes)
|
|
|
|
def _write_authors(self, comment_data: List[CommentType]) -> None:
|
|
# Write the <authors> element.
|
|
author_count = 0
|
|
|
|
self._xml_start_tag("authors")
|
|
|
|
for comment in comment_data:
|
|
author = comment.author
|
|
|
|
if author is not None and author not in self.author_ids:
|
|
# Store the author id.
|
|
self.author_ids[author] = author_count
|
|
author_count += 1
|
|
|
|
# Write the author element.
|
|
self._write_author(author)
|
|
|
|
self._xml_end_tag("authors")
|
|
|
|
def _write_author(self, data: str) -> None:
|
|
# Write the <author> element.
|
|
self._xml_data_element("author", data)
|
|
|
|
def _write_comment_list(self, comment_data: List[CommentType]) -> None:
|
|
# Write the <commentList> element.
|
|
self._xml_start_tag("commentList")
|
|
|
|
for comment in comment_data:
|
|
# Look up the author id.
|
|
author_id = -1
|
|
if comment.author is not None:
|
|
author_id = self.author_ids[comment.author]
|
|
|
|
# Write the comment element.
|
|
self._write_comment(comment, author_id)
|
|
|
|
self._xml_end_tag("commentList")
|
|
|
|
def _write_comment(self, comment: CommentType, author_id: int) -> None:
|
|
# Write the <comment> element.
|
|
ref = xl_rowcol_to_cell(comment.row, comment.col)
|
|
|
|
attributes = [("ref", ref)]
|
|
|
|
if author_id != -1:
|
|
attributes.append(("authorId", f"{author_id}"))
|
|
|
|
self._xml_start_tag("comment", attributes)
|
|
|
|
# Write the text element.
|
|
self._write_text(comment)
|
|
|
|
self._xml_end_tag("comment")
|
|
|
|
def _write_text(self, comment: CommentType) -> None:
|
|
# Write the <text> element.
|
|
self._xml_start_tag("text")
|
|
|
|
# Write the text r element.
|
|
self._write_text_r(comment)
|
|
|
|
self._xml_end_tag("text")
|
|
|
|
def _write_text_r(self, comment: CommentType) -> None:
|
|
# Write the <r> element.
|
|
self._xml_start_tag("r")
|
|
|
|
# Write the rPr element.
|
|
self._write_r_pr(comment)
|
|
|
|
# Write the text r element.
|
|
self._write_text_t(comment.text)
|
|
|
|
self._xml_end_tag("r")
|
|
|
|
def _write_text_t(self, text: str) -> None:
|
|
# Write the text <t> element.
|
|
attributes = []
|
|
|
|
if _preserve_whitespace(text):
|
|
attributes.append(("xml:space", "preserve"))
|
|
|
|
self._xml_data_element("t", text, attributes)
|
|
|
|
def _write_r_pr(self, comment: CommentType) -> None:
|
|
# Write the <rPr> element.
|
|
self._xml_start_tag("rPr")
|
|
|
|
# Write the sz element.
|
|
self._write_sz(comment.font_size)
|
|
|
|
# Write the color element.
|
|
self._write_color()
|
|
|
|
# Write the rFont element.
|
|
self._write_r_font(comment.font_name)
|
|
|
|
# Write the family element.
|
|
self._write_family(comment.font_family)
|
|
|
|
self._xml_end_tag("rPr")
|
|
|
|
def _write_sz(self, font_size: float) -> None:
|
|
# Write the <sz> element.
|
|
attributes = [("val", font_size)]
|
|
|
|
self._xml_empty_tag("sz", attributes)
|
|
|
|
def _write_color(self) -> None:
|
|
# Write the <color> element.
|
|
attributes = [("indexed", 81)]
|
|
|
|
self._xml_empty_tag("color", attributes)
|
|
|
|
def _write_r_font(self, font_name: str) -> None:
|
|
# Write the <rFont> element.
|
|
attributes = [("val", font_name)]
|
|
|
|
self._xml_empty_tag("rFont", attributes)
|
|
|
|
def _write_family(self, font_family: int) -> None:
|
|
# Write the <family> element.
|
|
attributes = [("val", font_family)]
|
|
|
|
self._xml_empty_tag("family", attributes)
|