Files
SemesterapparatsManager/src/utils/richtext.py
WorldTeacher 0406fe4f6f Refactor and enhance type hints across multiple modules
- Updated the `from_tuple` method in `Prof` class to specify return type.
- Added type hints for various methods in `LehmannsClient`, `OpenAI`, `WebRequest`, and `ZoteroController` classes to improve code clarity and type safety.
- Modified `pdf_to_csv` function to return a string instead of a DataFrame.
- Enhanced error handling and type hints in `wordparser` and `xmlparser` modules.
- Removed unused UI file `Ui_medianadder.ts`.
- Improved the layout and structure of the `semesterapparat_ui` to enhance user experience.
- Updated file picker to support `.doc` files in addition to `.docx`.
- Added unique item handling in `Ui` class to prevent duplicates in apparat list.
- General code cleanup and consistency improvements across various files.
2025-10-21 09:09:54 +02:00

372 lines
14 KiB
Python

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 <w:trPr> 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)