201 lines
8.0 KiB
Python
201 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
sipiq_thankyou_v1.3.py
|
||
======================
|
||
Verze: 1.3
|
||
Datum: 2026-06-18
|
||
Autor: Claude Code (pro MUDr. Vladimíra Buzalku)
|
||
|
||
Změna proti v1.2
|
||
----------------
|
||
- BEZ hlavičky From — do .eml se NEdává odesílací účet; JNJ Outlook doplní odesílatele
|
||
z GAL a odešle přes JNJ Exchange (jediná možnost). Pravidlo 18JUN2026. v1.2 v TRASH.
|
||
|
||
Změny proti v1.1
|
||
----------------
|
||
- DÁVKOVÝ REŽIM `--all` (+ `--skip prijmeni,...`): vygeneruje poděkování pro VŠECHNA
|
||
centra v sipiq_responses (iteruje po _id, ne po příjmení → rozliší i dva Konecnyovy).
|
||
Názvy souborů u shodného příjmení rozlišeny křestním jménem.
|
||
- `--email <pi_email>` pro adresné cílení jedné odpovědi.
|
||
- Kódování těla zůstává base64 (z v1.1) — Outlook čistě, žádné quoted-printable „=".
|
||
v1.0/v1.1 v TRASH.
|
||
|
||
Popis
|
||
-----
|
||
.eml draft poděkování za vyplnění SIPIQ (77242113UCO3002 / DAWN). Struktura BEZ Shrnutí:
|
||
úvod → podpis → ÚPLNĚ POD PODPISEM kompletní výpis SIPIQ (sekce → otázka → odpověď,
|
||
dle order/section, vynechána Confidentiality, jen neprázdné). To = email+email2 z investigators
|
||
(fallback pi_email), Cc = koordinátorky, from = its.jnj.com. Draft ≠ odesláno.
|
||
|
||
Použití:
|
||
python sipiq_thankyou_v1.3.py --last Bruncak --apply
|
||
python sipiq_thankyou_v1.3.py --email konecny.stefan@fnbrno.cz --apply
|
||
python sipiq_thankyou_v1.3.py --all --skip Bruncak --apply
|
||
Mongo 192.168.1.76:27017, bez auth, pymongo.
|
||
"""
|
||
import argparse
|
||
import os
|
||
import unicodedata
|
||
from datetime import datetime
|
||
from email.message import EmailMessage
|
||
|
||
from pymongo import MongoClient
|
||
|
||
OUT_DIR = r"u:\Dropbox\!!!Days\Downloads Z230\UploadToJNJ"
|
||
CC = "AKocourk@ITS.JNJ.com, EBartoso@its.jnj.com"
|
||
FROM = "vbuzalka@its.jnj.com"
|
||
SKIP_SECTIONS = {"Confidentiality Statement"}
|
||
SUBJECT = "77242113UCO3002 (DAWN) — poděkování za vyplnění feasibility dotazníku SIPIQ"
|
||
SIG = ("MUDr. Vladimír BUZALKA<br>"
|
||
"ICON plc / Performing Local Trial Management Services for Janssen – Cilag s.r.o. "
|
||
"/ Global Clinical Operations<br>"
|
||
"Mobile: +420 775 735 276 / Fax: +420 227 012 284<br>"
|
||
"E-mail: vbuzalka@its.jnj.com, vladimir.buzalka@iconplc.com")
|
||
|
||
|
||
def esc(s):
|
||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||
|
||
|
||
def ascii_slug(s):
|
||
return "".join(c for c in unicodedata.normalize("NFKD", str(s or ""))
|
||
if not unicodedata.combining(c)).lower().replace(" ", "")
|
||
|
||
|
||
def build_sipiq_html(db, resp):
|
||
ans = resp.get("answers") or {}
|
||
qs = list(db.sipiq_questions.find().sort("order", 1))
|
||
out, cur = [], None
|
||
for q in qs:
|
||
sec = q.get("section") or "Other"
|
||
if sec in SKIP_SECTIONS:
|
||
continue
|
||
items = q.get("items") or []
|
||
pairs = []
|
||
if not items:
|
||
v = ans.get(q["_id"])
|
||
if v not in (None, ""):
|
||
pairs.append((None, v))
|
||
else:
|
||
for it in items:
|
||
v = ans.get(it["key"])
|
||
if v not in (None, ""):
|
||
pairs.append((it.get("label") or "(odpověď)", v))
|
||
if not pairs:
|
||
continue
|
||
if sec != cur:
|
||
cur = sec
|
||
out.append(f'<h3 style="margin:16px 0 4px;color:#1F4E78">{esc(sec)}</h3>')
|
||
stem = esc((q.get("text") or "").replace("\n", " "))
|
||
out.append(f'<p style="margin:6px 0 1px"><b>[{q["_id"]}]</b> {stem}</p>')
|
||
if len(pairs) == 1 and pairs[0][0] is None:
|
||
out.append(f'<p style="margin:0 0 0 18px"><b>{esc(pairs[0][1])}</b></p>')
|
||
else:
|
||
out.append('<ul style="margin:2px 0 6px">')
|
||
for lbl, v in pairs:
|
||
out.append(f'<li>{esc(lbl)}: <b>{esc(v)}</b></li>' if lbl
|
||
else f'<li><b>{esc(v)}</b></li>')
|
||
out.append('</ul>')
|
||
return "\n".join(out)
|
||
|
||
|
||
def recipients(db, resp):
|
||
inv = db.investigators.find_one({"_id": resp.get("investigator_oid")},
|
||
{"email": 1, "email2": 1})
|
||
tos = [e for e in [(inv or {}).get("email"), (inv or {}).get("email2")] if e]
|
||
if not tos and resp.get("pi_email"):
|
||
tos = [resp["pi_email"]]
|
||
# vynech anonymizační proxy adresy (dd-proxy) — není to schránka, kterou lékař čte
|
||
tos = [e for e in tos if "dd-proxy" not in e.lower()] or tos
|
||
return ", ".join(tos)
|
||
|
||
|
||
def process_one(db, resp, dup_last, apply):
|
||
to = recipients(db, resp)
|
||
intro = (
|
||
'<p>Vážený pane doktore,</p>'
|
||
'<p>děkujeme za zaslání vyplněného dotazníku SIPIQ studie '
|
||
'<b>77242113UCO3002 (DAWN)</b>. Vaše odpovědi uvádíme pro úplnost v plném znění '
|
||
'níže, pod podpisem. Odeslaný dotazník již není možné editovat, takže kdybychom '
|
||
'při dalším zpracování našli položky chybějící, nejasné nebo nekonzistentní, '
|
||
'ještě bychom si vyžádali upřesnění e-mailem.</p>'
|
||
)
|
||
sig = f'<p>S pozdravem,<br>{SIG}</p>'
|
||
sipiq = build_sipiq_html(db, resp)
|
||
body = (intro + sig + '<hr>'
|
||
'<p style="color:#888;font-style:italic">Vyplněné SIPIQ (kopie Vašich odpovědí):</p>'
|
||
+ sipiq)
|
||
|
||
last = resp.get("pi_last_name") or "x"
|
||
first = resp.get("pi_first_name") or ""
|
||
slug = ascii_slug(last) + (("_" + ascii_slug(first)) if last in dup_last else "")
|
||
fname = f"podekovani_sipiq_{slug}_{datetime.now().strftime('%d%b%Y').upper()}.eml"
|
||
print(f" {first} {last:14} | {resp.get('site_country')} | To: {to[:55]}")
|
||
|
||
if not apply:
|
||
return None
|
||
msg = EmailMessage()
|
||
# ŽÁDNÝ From — JNJ Outlook doplní odesílatele z GAL (odešle přes JNJ Exchange)
|
||
msg["To"] = to
|
||
msg["Cc"] = CC
|
||
msg["Subject"] = SUBJECT
|
||
msg["X-Unsent"] = "1"
|
||
html_doc = (f'<html><body style="font-family:Calibri,Arial,sans-serif;font-size:11pt">'
|
||
f'{body}</body></html>')
|
||
msg.set_content(html_doc, subtype="html", charset="utf-8", cte="base64")
|
||
os.makedirs(OUT_DIR, exist_ok=True)
|
||
path = os.path.join(OUT_DIR, fname)
|
||
with open(path, "wb") as f:
|
||
f.write(bytes(msg))
|
||
return fname
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
g = ap.add_mutually_exclusive_group(required=True)
|
||
g.add_argument("--last", help="příjmení v sipiq_responses")
|
||
g.add_argument("--email", help="pi_email konkrétní odpovědi")
|
||
g.add_argument("--all", action="store_true", help="všechna centra")
|
||
ap.add_argument("--skip", default="", help="příjmení k vynechání (čárkou)")
|
||
ap.add_argument("--apply", action="store_true")
|
||
args = ap.parse_args()
|
||
|
||
db = MongoClient("mongodb://192.168.1.76:27017", serverSelectionTimeoutMS=8000).feasibility
|
||
skip = {s.strip() for s in args.skip.split(",") if s.strip()}
|
||
|
||
if args.all:
|
||
resps = [r for r in db.sipiq_responses.find() if r.get("pi_last_name") not in skip]
|
||
elif args.email:
|
||
resps = list(db.sipiq_responses.find({"pi_email": args.email.lower()}))
|
||
else:
|
||
resps = list(db.sipiq_responses.find({"pi_last_name": args.last}))
|
||
if not resps:
|
||
raise SystemExit("CHYBA: žádná odpovídající odpověď.")
|
||
if (args.last or args.email) and len(resps) > 1:
|
||
raise SystemExit(f"CHYBA: nejednoznačné ({len(resps)} odpovědí) — použij --email nebo --all.")
|
||
|
||
# duplicitní příjmení (kvůli disambiguaci názvů souborů)
|
||
seen, dup = set(), set()
|
||
for r in resps:
|
||
ln = r.get("pi_last_name")
|
||
if ln in seen:
|
||
dup.add(ln)
|
||
seen.add(ln)
|
||
|
||
resps.sort(key=lambda r: ((r.get("site_country") or ""), (r.get("pi_last_name") or "").lower()))
|
||
print(f"Center ke zpracování: {len(resps)}" + (f" (skip: {', '.join(skip)})" if skip else ""))
|
||
done = []
|
||
for r in resps:
|
||
fn = process_one(db, r, dup, args.apply)
|
||
if fn:
|
||
done.append(fn)
|
||
if not args.apply:
|
||
print(f"\n[DRY-RUN] {len(resps)} center, nic nezapsáno. Ostrý: --apply")
|
||
else:
|
||
print(f"\n[APPLY] Vygenerováno {len(done)} .eml do {OUT_DIR}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|