minor and major reworks: rename swb to SRU, add a test for pdf parsing

major: rework mail to send mail as plaintext instead of html, preventing the bleed-in of html text
This commit is contained in:
2025-10-07 14:15:10 +02:00
parent 0df7fd9fe6
commit 06965db26a
25 changed files with 1174 additions and 303 deletions

View File

@@ -12,20 +12,21 @@ __all__ = [
"ElsaAddEntry",
"ApparatExtendDialog",
"DocumentPrintDialog",
"NewEditionDialog",
"Settings",
]
from .about import About
from .app_ext import ApparatExtendDialog
from .bookdata import BookDataUI
from .docuprint import DocumentPrintDialog
from .elsa_add_entry import ElsaAddEntry
from .elsa_gen_confirm import ElsaGenConfirm
from .login import LoginDialog
from .mail import Mail_Dialog
from .mailTemplate import MailTemplateDialog
from .medienadder import MedienAdder
from .newEdition import NewEditionDialog
from .parsed_titles import ParsedTitles
from .popup_confirm import ConfirmDialog as popus_confirm
from .reminder import ReminderDialog
from .about import About
from .elsa_gen_confirm import ElsaGenConfirm
from .elsa_add_entry import ElsaAddEntry
from .app_ext import ApparatExtendDialog
from .docuprint import DocumentPrintDialog
from .settings import Settings

View File

@@ -2,7 +2,8 @@ from natsort import natsorted
from PySide6 import QtWidgets
from src import Icon
from src.backend import Database, Semester
from src.backend import Database
from src.logic import Semester
from src.utils.richtext import SemapSchilder, SemesterDocument
from .dialog_sources.documentprint_ui import Ui_Dialog

View File

@@ -1,4 +1,6 @@
import os
import re
import smtplib
import sys
import loguru
@@ -7,7 +9,7 @@ from PySide6 import QtWidgets
from src import LOG_DIR, Icon
from src import settings as config
from .dialog_sources.Ui_mail_preview import Ui_eMailPreview as MailPreviewDialog
from .dialog_sources.mail_preview_ui import Ui_eMailPreview as MailPreviewDialog
from .mailTemplate import MailTemplateDialog
log = loguru.logger
@@ -15,37 +17,61 @@ log.remove()
log.add(sys.stdout, level="INFO")
log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days")
CSS_RESET = "<style>html,body{margin:0;padding:0}p{margin:0}</style>"
empty_signature = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
empty_signature = """"""
<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style
type="text/css">
p, li { white-space: pre-wrap; }
def _escape_braces_in_style(html: str) -> str:
"""
Double curly braces ONLY inside <style>...</style> blocks so that
str.format(...) won't treat CSS as placeholders. The doubled braces
will automatically render back to single braces after formatting.
"""
hr { height: 1px; border-width: 0; }
def repl(m):
start, css, end = m.group(1), m.group(2), m.group(3)
css_escaped = css.replace("{", "{{").replace("}", "}}")
return f"{start}{css_escaped}{end}"
li.unchecked::marker { content: "\2610"; }
return re.sub(
r"(<style[^>]*>)(.*?)(</style>)",
repl,
html,
flags=re.IGNORECASE | re.DOTALL,
)
li.checked::marker { content: "\2612"; }
</style></head><body style=" font-family:''Segoe UI''; font-size:9pt; font-weight:400;
font-style:normal;">
def _split_eml_headers_body(eml_text: str) -> tuple[str, str]:
"""
Return (headers, body_html). Robustly split on first blank line.
Accepts lines that contain only spaces/tabs as the separator.
"""
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px;
margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>
"""
parts = re.split(r"\r?\n[ \t]*\r?\n", eml_text, maxsplit=1)
if len(parts) == 2:
return parts[0], parts[1]
# Fallback: try to split right after the Content-Transfer-Encoding line
m = re.search(
r"(?:^|\r?\n)Content-Transfer-Encoding:.*?(?:\r?\n)",
eml_text,
flags=re.I | re.S,
)
if m:
return eml_text[: m.end()], eml_text[m.end() :]
return "", eml_text # last resort: treat entire content as body
class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
def __init__(
self,
app_id,
app_name,
app_subject,
prof_name,
prof_mail,
app_id=None,
app_name=None,
app_subject=None,
prof_name=None,
prof_mail=None,
accepted_books=None,
ordered_books=None,
parent=None,
default_mail="Information zum Semesterapparat",
):
@@ -58,6 +84,7 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
self.subject = app_subject
self.profname = prof_name
self.books = accepted_books if accepted_books is not None else []
self.ordered_books = ordered_books if ordered_books is not None else []
self.mail_data = ""
self.signature = self.determine_signature()
self.prof_mail = prof_mail
@@ -65,52 +92,29 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog):
self.prof_name.setText(prof_name)
self.mail_name.setText(self.prof_mail)
self.load_mail_templates()
# if none of the radio buttons is checked, disable the accept button of the dialog
self.setWindowIcon(Icon("mail").icon)
self.btn_okay.setEnabled(False)
Icon("edit_note", self.newTemplate)
self.newTemplate.clicked.connect(self.open_new_template)
if default_mail is not None:
# get the nearest match to the default mail
for i in range(self.comboBox.count()):
if default_mail in self.comboBox.itemText(i):
default_mail = self.comboBox.itemText(i)
break
self.comboBox.setCurrentText(default_mail)
self.comboBox.currentIndexChanged.connect(self.set_mail)
# re-render when user changes greeting via radio buttons
self.gender_female.clicked.connect(self.set_mail)
self.gender_male.clicked.connect(self.set_mail)
self.gender_non.clicked.connect(self.set_mail)
# reflect initial state (OK disabled until a greeting is chosen)
self._update_ok_button()
self.btn_okay.clicked.connect(self.createAndSendMail)
def open_new_template(self):
log.info("Opening new template dialog")
# TODO: implement new mail template dialog
dialog = MailTemplateDialog()
dialog.updateSignal.connect(self.load_mail_templates)
dialog.exec()
pass
def determine_signature(self):
if config.mail.signature is empty_signature or config.mail.signature == "":
return """Mit freundlichen Grüßen
Ihr Semesterapparatsteam
Mail: semesterapparate@ph-freiburg.de
Tel.: 0761/682-778 | 07617682-545"""
else:
return config.mail.signature
def load_mail_templates(self):
# print("loading mail templates")
log.info("Loading mail templates")
mail_templates = os.listdir("mail_vorlagen")
log.info(f"Mail templates: {mail_templates}")
self.comboBox.clear()
for template in mail_templates:
self.comboBox.addItem(template)
# add these helpers inside Mail_Dialog
def get_greeting(self):
prof = self.profname.split(" ")[0]
if self.gender_male.isChecked():
@@ -124,45 +128,104 @@ Tel.: 0761/682-778 | 07617682-545"""
name = f"{self.profname.split(' ')[1]} {self.profname.split(' ')[0]}"
return f"Guten Tag {name},"
def _update_ok_button(self):
checked = (
self.gender_male.isChecked()
or self.gender_female.isChecked()
or self.gender_non.isChecked()
)
self.btn_okay.setEnabled(checked)
def _on_gender_toggled(self, checked: bool):
# Only refresh when a button becomes checked
if checked:
self.set_mail()
def open_new_template(self):
log.info("Opening new template dialog")
dialog = MailTemplateDialog()
dialog.updateSignal.connect(self.load_mail_templates)
dialog.exec()
def determine_signature(self):
# use equality, not identity
if (
config.mail.signature == empty_signature
or config.mail.signature.strip() == ""
):
return """Mit freundlichen Grüßen
Ihr Semesterapparatsteam
Mail: semesterapparate@ph-freiburg.de
Tel.: 0761/682-778 | 0761/682-545"""
else:
return config.mail.signature
def load_mail_templates(self):
log.info("Loading mail templates")
mail_templates = [
f for f in os.listdir("mail_vorlagen") if f.lower().endswith(".eml")
]
log.info(f"Mail templates: {mail_templates}")
self.comboBox.clear()
for template in mail_templates:
self.comboBox.addItem(template)
def set_mail(self):
log.info("Setting mail")
self._update_ok_button() # keep OK enabled state in sync
email_template = self.comboBox.currentText()
if email_template == "":
if not email_template:
log.debug("No mail template selected")
return
with open(f"mail_vorlagen/{email_template}", "r", encoding="utf-8") as f:
mail_template = f.read()
eml_text = f.read()
# header label for UI (unchanged)
email_header = email_template.split(".eml")[0]
if "{AppNr}" in email_template:
email_header = email_template.split(".eml")[0]
email_header = email_header.format(AppNr=self.appid, AppName=self.appname)
email_header = email_header.format(AppNr=self.appid, AppName=self.appname)
self.mail_header.setText(email_header)
self.mail_data = mail_template.split("<html>")[0]
mail_html = mail_template.split("<html>")[1]
mail_html = "<html>" + mail_html
Appname = self.appname
mail_html = mail_html.format(
Profname=self.profname.split(" ")[0],
Appname=Appname,
AppNr=self.appid,
AppSubject=self.subject,
greeting=self.get_greeting(),
signature=self.signature,
newEditions="<br>".join(
[
f"{book.title} von {book.author} (ISBN: {book.isbn}, Auflage: {book.edition}, In Bibliothek: {'ja' if getattr(book, 'library_location', 1) == 1 else 'nein'})"
for book in self.books
]
)
if self.books
else "keine neuen Auflagen gefunden",
)
self.mail_body.setHtml(mail_html)
headers, body_html = _split_eml_headers_body(eml_text)
body_html = _escape_braces_in_style(body_html)
# compute greeting from the current toggle selection
greeting = self.get_greeting()
try:
body_html = body_html.format(
Profname=self.profname.split(" ")[
0
], # last name if your template uses {Profname}
Appname=self.appname,
AppNr=self.appid,
AppSubject=self.subject,
greeting=greeting,
signature=self.signature,
newEditions="\n".join(
[
f"- {book.title} (ISBN: {','.join(book.isbn)}, Auflage: {book.edition if book.edition else 'nicht bekannt'}, In Bibliothek: {'ja' if getattr(book, 'signature', None) is not None and 'Handbibliothek' not in str(book.library_location) else 'nein'}, Typ: {book.get_book_type()}) Aktuelle Auflage: {book.old_book.edition if book.old_book and book.old_book.edition else 'nicht bekannt'}"
for book in (self.books or [])
]
)
if self.books
else "keine neuen Auflagen gefunden",
newEditionsOrdered="\n".join(
[
f" - {book.title}, ISBN: {','.join(book.isbn)}, Bibliotheksstandort : {book.library_location if book.library_location else 'N/A'}, Link: {book.link}"
for book in (self.ordered_books or [])
]
),
)
except Exception as e:
log.error(f"Template formatting failed: {e}")
self.mail_body.setPlainText(body_html)
def createAndSendMail(self):
log.info("Sending mail")
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@@ -176,31 +239,29 @@ Tel.: 0761/682-778 | 07617682-545"""
message["From"] = sender_email
message["To"] = self.prof_mail
message["Subject"] = self.mail_header.text()
# include a Fcc to the senders sent folder
message["cc"] = "semesterapparate@ph-freiburg.de"
message["Cc"] = "semesterapparate@ph-freiburg.de"
mail_body = self.mail_body.toPlainText()
# strange_string = """p, li { white-space: pre-wrap; }
# hr { height: 1px; border-width: 0; }
# li.unchecked::marker { content: "\2610"; }
# li.checked::marker { content: "\2612"; }
# """
# mail_body.replace(strange_string, "")
message.attach(MIMEText(mail_body, "Plain", "utf-8"))
mail_body = self.mail_body.toHtml()
message.attach(MIMEText(mail_body, "html"))
mail = message.as_string()
with smtplib.SMTP_SSL(smtp_server, port) as server:
server.connect(smtp_server, port)
# server.connect(smtp_server, port)
# server.auth(mechanism="PLAIN")
server.connect(smtp_server, port) # not needed for SMTP_SSL
if config.mail.use_user_name is True:
# print(config["mail"]["user_name"])
server.login(config.mail.user_name, password)
else:
server.login(sender_email, password)
server.sendmail(sender_email, tolist, mail)
# print("Mail sent")
# end active process
server.quit()
pass
log.info("Mail sent, closing connection to server and dialog")
# close the dialog
self.accept()
@@ -225,8 +286,6 @@ def launch_gui(
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Mail_Dialog()

View File

@@ -7,7 +7,7 @@ from qtqdm import Qtqdm, QtqdmProgressBar
from src.logic import BookData
from src.logic.lehmannsapi import LehmannsClient
from src.logic.swb import SWB
from src.logic.SRU import SWB
class CheckThread(QtCore.QThread):