This commit is contained in:
2026-05-20 15:25:25 +02:00
parent 049f589c21
commit b8543d1cab
15 changed files with 4483 additions and 44 deletions
+84 -28
View File
@@ -4,6 +4,8 @@ from datetime import datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
import tkinter as tk
from tkinter import simpledialog
load_dotenv(Path(__file__).parent / ".env")
@@ -23,7 +25,11 @@ SELECT_ROLE_URL = (
STUDY_NAME = "42847922MDD3003"
SITE_GROUP = "CZE"
FORM_NAME = "Date of Visit"
FORM_NAMES = [
"Date of Visit",
"Vital Signs",
"Interim Investigator Signature",
]
REPORT_ID = 92 # _EDC Std Rpt - Data Listing (Data Stream)
@@ -55,6 +61,21 @@ def dbg(page, label):
# Login
# ---------------------------------------------------------------------------
def _ask_otp_popup():
"""Zobrazí GUI dialog pro zadání OKTA OTP kódu."""
root = tk.Tk()
root.withdraw()
root.lift()
root.attributes("-topmost", True)
otp = simpledialog.askstring(
"OKTA MFA",
"Zadej OTP kód z OKTA (6 číslic):",
parent=root,
)
root.destroy()
return (otp or "").strip()
def do_login(page, context):
print("Přihlašuji se do iMedidata...")
page.goto(LOGIN_URL)
@@ -74,7 +95,10 @@ def do_login(page, context):
# OKTA MFA?
if _okta_mfa_present(page):
print("\n*** OKTA MFA vyžadována! ***")
otp = input("Zadej OTP kód z OKTA (6 číslic): ").strip()
otp = _ask_otp_popup()
if not otp:
print("CHYBA: OTP nebylo zadáno.")
sys.exit(1)
_fill_otp(page, otp)
# Čekáme na zpracování OTP a redirect zpět na iMedidata
wait_load(page, 3000)
@@ -136,7 +160,11 @@ def _fill_otp(page, otp):
def go_to_select_role(page):
"""Přejde na SelectRole stránku a vrátí True pokud jsme tam skutečně."""
print(f"Navigace na SelectRole...")
page.goto(SELECT_ROLE_URL)
try:
page.goto(SELECT_ROLE_URL)
except Exception:
# Rave dělá server-side redirect (ERR_ABORTED) — zkontrolujeme URL až po načtení
pass
wait_load(page, 1500)
dbg(page, "select-role")
return "login" not in page.url.lower() and "okta" not in page.url.lower()
@@ -267,36 +295,54 @@ def set_site_group_param(page):
dbg(page, "after-site-group")
def set_form_param(page):
"""Rozbalí Form panel, vyhledá Date of Visit a zaškrtne ho."""
print(f" Parametr Form: {FORM_NAME}")
def set_form_param(page, form_name):
"""Rozbalí Form panel (pokud je zavřený) a zaškrtne formulář.
Panel je SingleSelection=1, takže nový výběr automaticky odznačí předchozí."""
print(f" Parametr Form: {form_name}")
page.click('#PromptsBox_fm2_ShowHideBtn')
page.wait_for_timeout(2000)
# Otevřít panel jen pokud je zavřený (kontrola přes style.display)
is_closed = page.locator('#PromptsBox_fm2_div').evaluate('el => el.style.display') == 'none'
if is_closed:
page.click('#PromptsBox_fm2_ShowHideBtn')
page.wait_for_timeout(2000)
# Vyplnit search a odeslat Enterem — výsledek je okamžitý
page.wait_for_selector('#PromptsBox_fm2_SearchTxt', timeout=10_000)
page.fill('#PromptsBox_fm2_SearchTxt', FORM_NAME)
page.locator('#PromptsBox_fm2_SearchTxt').press('Enter')
page.wait_for_timeout(800)
# Po předchozím stažení je panel v "locked" módu.
# 1. klik na tužku → vymaže výběr, tlačítko se změní na oko
# 2. klik na oko → načte seznam všech formulářů
if page.locator('#PromptsBox_fm2_PageModeBtn').is_visible():
page.click('#PromptsBox_fm2_PageModeBtn') # tužka → oko
page.wait_for_timeout(1000)
page.click('#PromptsBox_fm2_PageModeBtn') # oko → načte formuláře
page.wait_for_timeout(2000)
# Zaškrtneme první (jediný) výsledek
cbs = page.query_selector_all('input[id^="PromptsBox_fm2_FrontEndCBList_"]')
if cbs:
if not cbs[0].is_checked():
cbs[0].click()
print(f" '{FORM_NAME}' zaškrtnuto")
wait_load(page, 500)
# Vyhledat formulář — klik zajistí focus, Enter spustí ajaxSelectionGridSearchBoxOnKeypress
search = page.locator('#PromptsBox_fm2_SearchTxt')
search.wait_for(state='visible', timeout=10_000)
search.click()
search.fill(form_name)
search.press('Enter')
# Počkáme až AJAX přepíše DOM se seznamem výsledků
cb_locator = page.locator('input[id^="PromptsBox_fm2_FrontEndCBList_"]').first
try:
cb_locator.wait_for(state='visible', timeout=8_000)
except PWTimeout:
print(f" VAROVÁNÍ: '{form_name}' nenalezen nebo timeout!")
return
print(f" VAROVÁNÍ: '{FORM_NAME}' nenalezen!")
# SingleSelection=1: klik na nový checkbox automaticky odznačí předchozí
# Locator se vyhodnotí čerstvě — žádný stale element handle
if not cb_locator.is_checked():
cb_locator.click()
print(f" '{form_name}' zaškrtnuto")
wait_load(page, 500)
# ---------------------------------------------------------------------------
# Submit a download
# ---------------------------------------------------------------------------
def submit_and_download(page, context):
def submit_and_download(page, context, form_name):
print("Odesílám report (čekám na nové okno)...")
with context.expect_page() as new_page_info:
@@ -350,7 +396,8 @@ def submit_and_download(page, context):
# Save as Unicode: necháme nezaškrtnuté (default)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
filename = f"{timestamp}_EDC_MDD3003_DataListing.csv"
form_slug = form_name.replace(" ", "")
filename = f"{timestamp}_EDC_MDD3003_{form_slug}_DataListing.csv"
output_path = DOWNLOAD_DIR / filename
print("Stahuji CSV...")
@@ -364,6 +411,13 @@ def submit_and_download(page, context):
download = dl_info.value
download.save_as(str(output_path))
print(f"\nHotovo! Soubor uložen: {output_path}")
try:
new_page.close()
print("Stahovací okno zavřeno.")
except Exception:
pass
return output_path
@@ -409,17 +463,19 @@ def run():
# Krok 6: otevření reportu
open_report(page)
# Krok 7: nastavení parametrů
# Krok 7: nastavení parametrů (Study a Site Group jednou, Form v smyčce)
print("Nastavuji parametry reportu...")
set_study_param(page)
set_site_group_param(page)
set_form_param(page)
# Krok 8: odeslání a stažení
output = submit_and_download(page, context)
# Krok 8: smyčka přes formuláře
for form_name in FORM_NAMES:
print(f"\n=== Stahuji formulář: {form_name} ===")
set_form_param(page, form_name)
submit_and_download(page, context, form_name)
input("\nZmáčkni Enter pro zavření prohlížeče...")
browser.close()
print("Prohlížeč zavřen.")
if __name__ == "__main__":