import os import sys 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") USERNAME = os.getenv("IMEDIDATA_USERNAME", "vladimir.buzalka") PASSWORD = os.getenv("IMEDIDATA_PASSWORD", "") DOWNLOAD_DIR = Path(__file__).parent / "downloads" AUTH_FILE = Path(__file__).parent / "auth.json" AUTH_MAX_AGE_DAYS = 7 LOGIN_URL = "https://login.imedidata.com/login" SELECT_ROLE_URL = ( "https://jnjja.mdsol.com/MedidataRave/SelectRole.aspx" "?client_division_uuid=e5de55d5-a414-4bd1-9abe-18e96fd5475d" "&study_group_uuid=b0793ca6-33ec-44e8-883b-6fc1a4b671c4" "&studygroup_id=107981" ) STUDY_NAME = "42847922MDD3003" SITE_GROUP = "CZE" REPORT_ID = 164 # _EDC Std Rpt - Query Details (Data Stream) # Query Status: libovolná kombinace z ["Open", "Answered", "Closed", "Canceled"] QUERY_STATUSES = [] # prázdné = Default: All (nefiltrovat) # Milestone: vždy dostupný "Final", ostatní závisí na studii MILESTONES = ["Final"] # Datum ve formátu DD-Mon-YYYY (např. "01-Jan-2024"), prázdný řetězec = bez filtru START_DATE = "" END_DATE = "" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def auth_valid(): if not AUTH_FILE.exists(): return False age = datetime.now() - datetime.fromtimestamp(AUTH_FILE.stat().st_mtime) return age < timedelta(days=AUTH_MAX_AGE_DAYS) def wait_load(page, extra_ms=1000): try: page.wait_for_load_state("load", timeout=20_000) except PWTimeout: pass page.wait_for_timeout(extra_ms) def dbg(page, label): print(f"[{label}] URL: {page.url}") # --------------------------------------------------------------------------- # Login # --------------------------------------------------------------------------- def _ask_otp_popup(): 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) wait_load(page, 500) dbg(page, "login-page") page.wait_for_selector('input[name="session[username]"]', timeout=10_000) page.fill('input[name="session[username]"]', USERNAME) page.fill('input[name="session[password]"]', PASSWORD) page.click('button[type="submit"]') wait_load(page, 2000) dbg(page, "after-signin") if _okta_mfa_present(page): print("\n*** OKTA MFA vyžadována! ***") otp = _ask_otp_popup() if not otp: print("CHYBA: OTP nebylo zadáno.") sys.exit(1) _fill_otp(page, otp) wait_load(page, 3000) dbg(page, "after-otp") try: page.wait_for_url("**/home.imedidata.com**", timeout=30_000) except PWTimeout: dbg(page, "wait-home-timeout") dbg(page, "final-login") if "home.imedidata.com" not in page.url: print("CHYBA: Přihlášení se nezdařilo!") input("Zmáčkni Enter pro ukončení...") sys.exit(1) context.storage_state(path=str(AUTH_FILE)) print("Session uložena do auth.json") def _okta_mfa_present(page): if "okta" in page.url.lower(): return True for sel in [ 'input[name="answer"]', 'input[name*="otp"]', 'input[name*="code"]', 'input[placeholder*="code" i]', ]: try: if page.query_selector(sel): return True except Exception: # Page navigated during selector, skip pass return False def _fill_otp(page, otp): for sel in [ 'input[name="answer"]', 'input[name*="otp"]', 'input[name*="code"]', 'input[type="tel"]', 'input[placeholder*="code" i]', ]: try: el = page.query_selector(sel) if el: el.fill(otp) page.keyboard.press("Enter") return except Exception: # Page navigated, continue to next selector pass try: page.keyboard.type(otp) page.keyboard.press("Enter") except Exception: pass # --------------------------------------------------------------------------- # Navigace # --------------------------------------------------------------------------- def go_to_select_role(page): print("Navigace na SelectRole...") try: page.goto(SELECT_ROLE_URL) except Exception: pass wait_load(page, 1500) dbg(page, "select-role") return "login" not in page.url.lower() and "okta" not in page.url.lower() def select_role(page): print("Vybírám roli Site Manager...") try: page.wait_for_selector("select", timeout=10_000) except PWTimeout: return selects = page.query_selector_all("select") found = False for sel_el in selects: opts = sel_el.query_selector_all("option") for opt in opts: txt = (opt.inner_text() or "").strip() if "site manager" in txt.lower(): sel_el.select_option(label=txt) found = True print(f" Vybráno: '{txt}'") break if found: break if not found: try: page.get_by_text("Site Manager", exact=False).first.click() except Exception as e: print(f" {e}") for btn_sel in [ 'input[value="Continue"]', 'input[type="submit"]', 'button:has-text("Continue")', 'button[type="submit"]', ]: try: btn = page.query_selector(btn_sel) if btn: btn.click() break except Exception: continue wait_load(page, 2000) dbg(page, "after-role") def navigate_to_reporter(page): print("Klikám na Reporter...") try: page.wait_for_selector('a:has-text("Reporter")', timeout=15_000) page.click('a:has-text("Reporter")') wait_load(page, 1500) dbg(page, "reporter") except PWTimeout: dbg(page, "reporter-not-found") raise def open_report(page): print(f"Klikám na report ID={REPORT_ID} (Query Details)...") selector = f'a[href="PromptsPage.aspx?ReportID={REPORT_ID}"]' try: page.wait_for_selector(selector, timeout=15_000) page.click(selector) wait_load(page, 2000) dbg(page, "report-opened") except PWTimeout: dbg(page, "report-not-found") raise # --------------------------------------------------------------------------- # Parametry reportu # --------------------------------------------------------------------------- def set_study_param(page): print(f" Parametr Study: {STUDY_NAME}") page.click('#PromptsBox_st_ShowHideBtn') page.wait_for_timeout(1500) page.wait_for_selector('#PromptsBox_st_FrontEndCBList_0', timeout=10_000) cb = page.locator('#PromptsBox_st_FrontEndCBList_0') if not cb.is_checked(): cb.check() wait_load(page, 3000) dbg(page, "after-study") def set_site_group_param(page): print(f" Parametr Site Group: {SITE_GROUP}") page.click('#PromptsBox_sg_ShowHideBtn') page.wait_for_timeout(1500) page.wait_for_selector('#PromptsBox_sg_List', timeout=10_000) page.select_option('#PromptsBox_sg_List', label=SITE_GROUP) page.evaluate("document.querySelector('#PromptsBox_sg_List').dispatchEvent(new Event('change', {bubbles:true}))") wait_load(page, 2000) print(" Include Sub Site Groups: zapnuto") cb = page.locator('#PromptsBox_sg_CheckBox') if not cb.is_checked(): cb.check() page.evaluate("document.querySelector('#PromptsBox_sg_CheckBox').dispatchEvent(new Event('change', {bubbles:true}))") wait_load(page, 2000) page.click('#PromptsBox_sg_ShowHideBtn') wait_load(page, 3000) dbg(page, "after-site-group") def set_query_status_param(page): if not QUERY_STATUSES: print(" Parametr Query Status: All (přeskočeno)") return print(f" Parametr Query Status: {', '.join(QUERY_STATUSES)}") page.click('#PromptsBox_qu_ShowHideBtn') page.wait_for_timeout(1500) # Počkáme na načtení checkboxů page.wait_for_selector('input[id^="PromptsBox_qu_FrontEndCBList_"]', timeout=10_000) # Zaškrtneme požadované statusy podle labelu label_map = {"Open": 0, "Answered": 1, "Closed": 2, "Canceled": 3} for status in QUERY_STATUSES: idx = label_map.get(status) if idx is None: print(f" VAROVÁNÍ: neznámý status '{status}'") continue cb = page.locator(f'#PromptsBox_qu_FrontEndCBList_{idx}') if not cb.is_checked(): cb.check() print(f" '{status}' zaškrtnuto") wait_load(page, 1000) def set_milestone_param(page): print(f" Parametr Milestone: {', '.join(MILESTONES)}") # Otevřít panel pokud je zavřený is_closed = page.locator('#PromptsBox_ms_div').evaluate('el => el.style.display') == 'none' if is_closed: page.click('#PromptsBox_ms_ShowHideBtn') page.wait_for_timeout(2000) # Po předchozím výběru: tužka → oko → načtení seznamu if page.locator('#PromptsBox_ms_PageModeBtn').is_visible(): page.click('#PromptsBox_ms_PageModeBtn') # tužka → oko page.wait_for_timeout(1000) page.click('#PromptsBox_ms_PageModeBtn') # oko → načte milestony page.wait_for_timeout(2000) for milestone in MILESTONES: search = page.locator('#PromptsBox_ms_SearchTxt') search.wait_for(state='visible', timeout=10_000) search.click() search.fill(milestone) search.press('Enter') cb = page.locator('input[id^="PromptsBox_ms_FrontEndCBList_"]').first try: cb.wait_for(state='visible', timeout=8_000) except PWTimeout: print(f" VAROVÁNÍ: '{milestone}' nenalezen!") continue if not cb.is_checked(): cb.click() print(f" '{milestone}' zaškrtnuto") wait_load(page, 500) def set_date_param(page, panel_id, date_value, label): if not date_value: return print(f" Parametr {label}: {date_value}") page.click(f'#{panel_id}_ShowHideBtn') page.wait_for_timeout(1000) date_input = page.locator(f'#{panel_id}_DatePickerTxt') date_input.wait_for(state='visible', timeout=10_000) date_input.click() date_input.fill(date_value) date_input.press('Tab') page.wait_for_timeout(500) # --------------------------------------------------------------------------- # Submit a download # --------------------------------------------------------------------------- def submit_and_download(page, context): print("Odesílám report (čekám na nové okno)...") with context.expect_page() as new_page_info: page.locator('input[value="Submit Report"], button:has-text("Submit Report")').first.click() new_page = new_page_info.value new_page.wait_for_url(lambda url: url != 'about:blank', timeout=30_000) print(" Čekám na vygenerování reportu...") new_page.wait_for_selector( 'input[value="Download File"], button:has-text("Download File")', timeout=300_000 ) new_page.wait_for_timeout(500) dbg(new_page, "download-window") print(" Nastavuji parametry stahování...") target_frame = new_page.main_frame for frame in new_page.frames: if frame.query_selector('select') or frame.query_selector('input[value="Download File"]'): target_frame = frame print(f" Frame nalezen: {frame.url}") break for sel in target_frame.query_selector_all('select'): for opt in sel.query_selector_all('option'): val = opt.get_attribute('value') or '' txt = opt.inner_text() or '' if 'vnd.ms-excel' in val or 'vnd.ms-excel' in txt: sel.select_option(value=val) print(" File type: .csv (application/vnd.ms-excel)") break for sel in target_frame.query_selector_all('select'): for opt in sel.query_selector_all('option'): if 'attachment' in (opt.get_attribute('value') or '').lower(): sel.select_option(value='attachment') break timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M") filename = f"{timestamp}_EDC_MDD3003_QueryDetails.csv" output_path = DOWNLOAD_DIR / filename print("Stahuji CSV...") with new_page.expect_download(timeout=60_000) as dl_info: btn = target_frame.query_selector('input[value="Download File"], button:has-text("Download File")') if btn: btn.click() else: new_page.locator('input[value="Download File"], button:has-text("Download File")').first.click() 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 # --------------------------------------------------------------------------- # Hlavní flow # --------------------------------------------------------------------------- def run(): if not PASSWORD: print("Chyba: nastav IMEDIDATA_PASSWORD v souboru .env") sys.exit(1) DOWNLOAD_DIR.mkdir(exist_ok=True) with sync_playwright() as p: browser = p.chromium.launch(headless=False, slow_mo=200) ctx_kwargs = {"accept_downloads": True} use_saved = auth_valid() if use_saved: print("Načítám uloženou session (auth.json)...") ctx_kwargs["storage_state"] = str(AUTH_FILE) context = browser.new_context(**ctx_kwargs) page = context.new_page() logged_in = go_to_select_role(page) if not logged_in: if use_saved: print("Session expirovala, mažu auth.json a přihlašuji znovu...") AUTH_FILE.unlink(missing_ok=True) do_login(page, context) go_to_select_role(page) select_role(page) navigate_to_reporter(page) open_report(page) print("Nastavuji parametry reportu...") set_study_param(page) set_site_group_param(page) set_query_status_param(page) set_milestone_param(page) set_date_param(page, 'PromptsBox_sd', START_DATE, "Start Date") set_date_param(page, 'PromptsBox_ed', END_DATE, "End Date") submit_and_download(page, context) browser.close() print("Prohlížeč zavřen.") if __name__ == "__main__": run()