from datetime import datetime from docx import Document from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.shared import Pt, RGBColor, Cm from docx.oxml import OxmlElement from docx.oxml.ns import qn import os from os.path import basename from loguru import logger as log import sys logger = log logger.remove() logger.add("logs/application.log", rotation="1 week", enqueue=True) log.add( f"logs/{datetime.now().strftime('%Y-%m-%d')}.log", rotation="1 day", compression="zip", ) # logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") logger.add(sys.stdout) class SemesterError(Exception): """Custom exception for semester-related errors.""" def __init__(self, message): super().__init__(message) logger.error(message) def __str__(self): return f"SemesterError: {self.args[0]}" class SemesterDocument: def __init__( self, apparats: list[tuple[int, str]], semester: str, filename, config, full: bool = False, ): assert isinstance(apparats, list), SemesterError( "Apparats must be a list of tuples" ) assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError( "Apparats must be a list of tuples" ) assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError( "Apparat numbers must be integers" ) assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError( "Apparat names must be strings" ) assert isinstance(semester, str), SemesterError("Semester must be a string") assert "." not in filename and isinstance(filename, str), SemesterError( "Filename must be a string and not contain an extension" ) self.doc = Document() self.apparats = apparats self.semester = semester self.table_font = "Arial" self.header_font = "Times New Roman" self.header_font_size = Pt(26) self.sub_header_font_size = Pt(18) self.table_font_size = Pt(10) self.color_red = RGBColor(255, 0, 0) self.color_blue = RGBColor(0, 0, 255) self.filename = filename self.settings = config if full: logger.info("Full document generation") self.make_document() logger.info("Document created") self.create_pdf() logger.info("PDF created") self.print_document() logger.info("Document printed") self.cleanup() logger.info("Cleanup done") def set_table_border(self, table): """ Adds a full border to the table. :param table: Table object to which the border will be applied. """ tbl = table._element tbl_pr = tbl.xpath("w:tblPr")[0] tbl_borders = OxmlElement("w:tblBorders") # Define border styles for border_name in ["top", "left", "bottom", "right", "insideH", "insideV"]: border = OxmlElement(f"w:{border_name}") border.set(qn("w:val"), "single") border.set(qn("w:sz"), "4") # Thickness of the border border.set(qn("w:space"), "0") border.set(qn("w:color"), "000000") # Black color tbl_borders.append(border) tbl_pr.append(tbl_borders) def create_sorted_table(self) -> None: # Sort the apparats list by the string in the tuple (index 1) self.apparats.sort(key=lambda x: x[1]) # Create a table with rows equal to the length of the apparats list table = self.doc.add_table(rows=len(self.apparats), cols=2) table.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # Set column widths by directly modifying the cell properties widths = [Cm(1.19), Cm(10.39)] for col_idx, width in enumerate(widths): for cell in table.columns[col_idx].cells: cell_width_element = cell._element.xpath(".//w:tcPr")[0] tcW = OxmlElement("w:tcW") tcW.set(qn("w:w"), str(int(width.cm * 567))) # Convert cm to twips tcW.set(qn("w:type"), "dxa") cell_width_element.append(tcW) # Adjust row heights for row in table.rows: trPr = row._tr.get_or_add_trPr() # Get or add the element trHeight = OxmlElement("w:trHeight") trHeight.set( qn("w:val"), str(int(Pt(15).pt * 20)) ) # Convert points to twips trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height trPr.append(trHeight) # Fill the table with sorted data for row_idx, (number, name) in enumerate(self.apparats): row = table.rows[row_idx] # Set font for the first column (number) cell_number_paragraph = row.cells[0].paragraphs[0] cell_number_run = cell_number_paragraph.add_run(str(number)) cell_number_run.font.name = self.table_font cell_number_run.font.size = self.table_font_size cell_number_run.font.bold = True cell_number_run.font.color.rgb = self.color_red cell_number_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # Set font for the second column (name) cell_name_paragraph = row.cells[1].paragraphs[0] words = name.split() if words: # Add the first word in bold bold_run = cell_name_paragraph.add_run(words[0]) bold_run.font.bold = True bold_run.font.name = self.table_font bold_run.font.size = self.table_font_size # Add the rest of the words normally if len(words) > 1: normal_run = cell_name_paragraph.add_run(" " + " ".join(words[1:])) normal_run.font.name = self.table_font normal_run.font.size = self.table_font_size cell_name_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT self.set_table_border(table) def make_document(self): # Create a new Document section = self.doc.sections[0] section.top_margin = Cm(2.54) # Default 1 inch (can adjust as needed) section.bottom_margin = Cm(1.5) # Set bottom margin to 1.5 cm section.left_margin = Cm(2.54) # Default 1 inch section.right_margin = Cm(2.54) # Default 1 inch # Add the current date current_date = datetime.now().strftime("%Y-%m-%d") date_paragraph = self.doc.add_paragraph(current_date) date_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT # Add a header semester = f"Semesterapparate {self.semester}" header = self.doc.add_paragraph(semester) header.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER header_run = header.runs[0] header_run.font.name = self.header_font header_run.font.size = self.header_font_size header_run.font.bold = True header_run.font.color.rgb = self.color_blue sub_header = self.doc.add_paragraph("(Alphabetisch)") sub_header.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER sub_header_run = sub_header.runs[0] sub_header_run.font.name = self.header_font sub_header_run.font.size = self.sub_header_font_size sub_header_run.font.color.rgb = self.color_red self.doc.add_paragraph("") self.create_sorted_table() def save_document(self, name): # Save the document self.doc.save(name) print(f"Document saved as {name}") def print_document(self): # send document to printer as attachment of email import smtplib from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from email.mime.text import MIMEText config = self.settings smtp = config.mail.smtp_server port = config.mail.port sender_email = config.mail.sender password = config.mail.password receiver = config.mail.printer_mail message = MIMEMultipart() message["From"] = sender_email message["To"] = receiver message["cc"] = config.mail.sender message["Subject"] = "." mail_body = "." message.attach(MIMEText(mail_body, "html")) with open(self.filename + ".pdf", "rb") as fil: part = MIMEApplication(fil.read(), Name=basename(self.filename + "pdf")) # After the file is closed part["Content-Disposition"] = 'attachment; filename="%s"' % basename( self.filename + ".pdf" ) message.attach(part) mail = message.as_string() with smtplib.SMTP_SSL(smtp, port) as server: server.connect(smtp, port) server.login(config.mail.user_name, password) server.sendmail(sender_email, receiver, mail) server.quit() print("Mail sent") def create_pdf(self): # Save the document import comtypes.client word = comtypes.client.CreateObject("Word.Application") self.save_document(self.filename + ".docx") docpath = os.path.abspath(self.filename + ".docx") doc = word.Documents.Open(docpath) curdir = os.getcwd() doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.Close() word.Quit() logger.debug("PDF saved") def cleanup(self): os.remove(f"{self.filename}.docx") os.remove(f"{self.filename}.pdf") if __name__ == "__main__": pass # apparat = [(i, f"Item {i}") for i in range(405, 438)] # doc = SemesterDocument( # apparat, # "WiSe 24/25", # "semap", # ) # doc.make_document() # doc.create_pdf() # doc.print_document() # def printers(): # printers = win32print.EnumPrinters( # win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS # ) # for i, printer in enumerate(printers): # print(f"{i}: {printer[2]}") # list printers