import os from datetime import datetime from os.path import basename from docx import Document from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Cm, Pt, RGBColor from src import settings from src.shared.logging import log logger = log font = "Cascadia Mono" def print_document(file: str) -> None: # send document to printer as attachment of email import smtplib from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText smtp = settings.mail.smtp_server port = settings.mail.port sender_email = settings.mail.sender password = settings.mail.password receiver = settings.mail.printer_mail message = MIMEMultipart() message["From"] = sender_email message["To"] = receiver message["cc"] = settings.mail.sender message["Subject"] = "." mail_body = "." message.attach(MIMEText(mail_body, "html")) with open(file, "rb") as fil: part = MIMEApplication(fil.read(), Name=basename(file)) # After the file is closed part["Content-Disposition"] = 'attachment; filename="%s"' % basename(file) message.attach(part) mail = message.as_string() with smtplib.SMTP_SSL(smtp, port) as server: server.connect(smtp, port) server.login(settings.mail.user_name, password) server.sendmail(sender_email, receiver, mail) server.quit() log.success("Mail sent") class SemesterError(Exception): """Custom exception for semester-related errors.""" def __init__(self, message: str): super().__init__(message) log.error(message) def __str__(self): return f"SemesterError: {self.args[0]}" class SemesterDocument: def __init__( self, apparats: list[tuple[int, str]], semester: str, filename: str, 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_normal = font self.table_font_bold = font self.header_font = font 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 if full: log.info("Full document generation") self.cleanup log.info("Cleanup done") self.make_document() log.info("Document created") self.create_pdf() log.info("PDF created") print_document(self.filename + ".pdf") log.info("Document printed") 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)] 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_bold 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 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 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: str) -> None: # Save the document self.doc.save(name) def create_pdf(self) -> None: # Save the document import comtypes.client word = comtypes.client.CreateObject("Word.Application") # type: ignore 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() log.debug("PDF saved") @property def cleanup(self) -> None: if os.path.exists(f"{self.filename}.docx"): os.remove(f"{self.filename}.docx") os.remove(f"{self.filename}.pdf") @property def send(self) -> None: print_document(self.filename + ".pdf") log.debug("Document sent to printer") class SemapSchilder: def __init__(self, entries: list[str]): self.entries = entries self.filename = "Schilder" self.font_size = Pt(23) self.font_name = font self.doc = Document() self.define_doc_properties() self.add_entries() self.cleanup() self.create_pdf() def define_doc_properties(self): # set the doc to have a top margin of 1cm, left and right are 0.5cm, bottom is 0cm section = self.doc.sections[0] section.top_margin = Cm(1) section.bottom_margin = Cm(0) section.left_margin = Cm(0.5) section.right_margin = Cm(0.5) # set the font to Times New Roman, size 23 bold, color black for paragraph in self.doc.paragraphs: for run in paragraph.runs: run.font.name = self.font_name run.font.size = self.font_size run.font.bold = True run.font.color.rgb = RGBColor(0, 0, 0) paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # if the length of the text is def add_entries(self): for entry in self.entries: paragraph = self.doc.add_paragraph(entry) paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER paragraph.paragraph_format.line_spacing = Pt(23) # Set fixed line spacing paragraph.paragraph_format.space_before = Pt(2) # Remove spacing before paragraph.paragraph_format.space_after = Pt(2) # Remove spacing after run = paragraph.runs[0] run.font.name = self.font_name run.font.size = self.font_size run.font.bold = True run.font.color.rgb = RGBColor(0, 0, 0) # Add a line to be used as a guideline for cutting line = self.doc.add_paragraph() line.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER line.paragraph_format.line_spacing = Pt(23) # Match line spacing line.paragraph_format.space_before = Pt(2) # Remove spacing before line.paragraph_format.space_after = Pt(2) # Remove spacing after line.add_run("--------------------------") def save_document(self): # Save the document self.doc.save(f"{self.filename}.docx") log.debug(f"Document saved as {self.filename}.docx") def create_pdf(self) -> None: # Save the document import comtypes.client word = comtypes.client.CreateObject("Word.Application") # type: ignore self.save_document() docpath = os.path.abspath(f"{self.filename}.docx") doc = word.Documents.Open(docpath) curdir = os.getcwd() doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.Close() word.Quit() log.debug("PDF saved") def cleanup(self) -> None: if os.path.exists(f"{self.filename}.docx"): os.remove(f"{self.filename}.docx") if os.path.exists(f"{self.filename}.pdf"): os.remove(f"{self.filename}.pdf") @property def send(self) -> None: print_document(self.filename + ".pdf") log.debug("Document sent to printer") if __name__ == "__main__": entries = [ "Lüsebrink (Theorie und Praxis der Leichtathletik)", "Kulovics (ISP-Betreuung)", "Köhler (Ausgewählte Aspekte der materiellen Kultur Textil)", "Grau (Young Adult Literature)", "Schiebel (Bewegung II:Ausgewählte Problemfelder)", "Schiebel (Ernährungswiss. Perspektive)", "Park (Kommunikation und Kooperation)", "Schiebel (Schwimmen)", "Huppertz (Philosophieren mit Kindern)", "Heyl (Heyl)", "Reuter (Verschiedene Veranstaltungen)", "Reinhold (Arithmetik und mathematisches Denken)", "Wirtz (Forschungsmethoden)", "Schleider (Essstörungen)", "Schleider (Klinische Psychologie)", "Schleider (Doktorandenkolloquium)", "Schleider (Störungen Sozialverhaltens/Delinquenz)", "Burth (EU Forschung im Int. Vergleich/EU Gegenstand biling. Didaktik)", "Reinhardt (Einführung Politikdidaktik)", "Schleider (Psychologische Interventionsmethoden)", "Schleider (ADHS)", "Schleider (Beratung und Teamarbeit)", "Schleider (LRS)", "Schleider (Gesundheitspsychologie)", "Schleider (Elterntraining)", "Wulff (Hochschulzertifikat DaZ)", "Dinkelaker ( )", "Droll (Einführung in die Sprachwissenschaft)", "Karoß (Gymnastik - Sich Bewegen mit und ohne Handgeräte)", "Sahrai (Kindheit und Gesellschaft)", ] doc = SemapSchilder(entries)