z230
This commit is contained in:
@@ -1,22 +1,10 @@
|
||||
{
|
||||
"sel_status": "Všechna",
|
||||
"sel_proto": "77242113UCO3001",
|
||||
"sel_status": "Aktivní",
|
||||
"sel_proto": "42847922MDD3003",
|
||||
"sel_role": [
|
||||
"Principal Investigator",
|
||||
"Sub-Investigator",
|
||||
"Study Coordinator"
|
||||
],
|
||||
"sel_site": [
|
||||
"DD5-CZ10001",
|
||||
"DD5-CZ10003",
|
||||
"DD5-CZ10006",
|
||||
"DD5-CZ10009",
|
||||
"DD5-CZ10010",
|
||||
"DD5-CZ10012",
|
||||
"DD5-CZ10013",
|
||||
"DD5-CZ10015",
|
||||
"DD5-CZ10016",
|
||||
"DD5-CZ10020",
|
||||
"DD5-CZ10021"
|
||||
]
|
||||
"sel_site": []
|
||||
}
|
||||
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,472 @@
|
||||
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]',
|
||||
]:
|
||||
if page.query_selector(sel):
|
||||
return True
|
||||
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]',
|
||||
]:
|
||||
el = page.query_selector(sel)
|
||||
if el:
|
||||
el.fill(otp)
|
||||
page.keyboard.press("Enter")
|
||||
return
|
||||
page.keyboard.type(otp)
|
||||
page.keyboard.press("Enter")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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()
|
||||
+84
-28
@@ -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__":
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
"Country/Region","Site Number","Sites","Subjects","Subject Status","Visits","Pages","Queries (Op/Ans/SDV)","RecordPosition","Field","Query Group","QueryID(ReQry)","QueryStatus","Opened By","Opened Date","Answered By","Answered Date","Closed By","Closed Date","DaysNotYetClosed","Days to Answer","Days to Close","QueryText","Answer Text (if any)","StudyParameter","SiteGroupParameter","SiteNumberParameter","SiteParameter","SubjectParameter","SubjectStatusParameter","FolderParameter","FormParameter","FieldParameter","MarkingGroupParameter","QueryStatusParameter","IncludeInactivePagesParameter","PageSDVParameter","PageFrozenParameter","PageLockedParameter","StartDateParameter","EndDateParameter","MilestoneParameter","ReportTypeParameter","VersionNumber","PrintDateTime","TimeZone","RunUser","ErrorString","OpenedDateSrtble","AnsweredDateSrtble","ClosedDateSrtble","VisitSiteLevel","VisitCountryLevel","VisitStudyLevel","PageSubjectLevel","PageSiteLevel","PageCountryLevel","PageStudyLevel"
|
||||
"CZE","S10-CZ10004","Medical Services Prague s.r.o.","CZ100040006","Enrolled","Visit 8 Part 1 (1)","Date of Visit","0/1/N","","Did this visit occur?","DM","20915933","Answered","shyamaprasad972","19 May 2026 12:47:10:123","EHerman2","20 May 2026 12:02:23:567","","","1","1","","Medical Review I-12758914: Kindly provide the clinical significance of high triglyceride as there is reported hypercholesterolemia in MH page. Kindly clarify if there is any worsening of existing MH.","not clinically significant","42847922MDD3003 [Prod]","CZE","All","All","All","None","All","All","All","All","Open,Answered","No","None","None","None","None","None","Final","With Text","5.6 - 1.0","5/20/2026 1:21:29 PM","GMT","Vladimir Buzalka [Site Manager]","","20260519 12:47:10.123","20260520 12:02:23.567","","1","4","4","1","1","4","4"
|
||||
"CZE","S10-CZ10011","MEDIPA s r o","CZ100110008","Enrolled","Visit 1.7 Induction (1)","Date of Visit","1/0/N","","Subject's Status","DM","20911926","Open(R)","shyamaprasad972","19 May 2026 11:26:37:217","","","","","1","","","Medical Review I-12758916: Please comment on the clinical significance of higher-than-normal levels of GGT.","","42847922MDD3003 [Prod]","CZE","All","All","All","None","All","All","All","All","Open,Answered","No","None","None","None","None","None","Final","With Text","5.6 - 1.0","5/20/2026 1:21:29 PM","GMT","Vladimir Buzalka [Site Manager]","","20260519 11:26:37.217","","","2","4","4","1","2","4","4"
|
||||
"CZE","S10-CZ10011","MEDIPA s r o","CZ100110010","Enrolled","Study Drug Administration (1)","Study Drug Administration Open Label Part 2 - Stabilization","1/0/R","1","Start Date","Auto","20908883","Open(R)","systemuser","19 May 2026 09:28:33:810","","","","","1","","","Please enter study drug administration start date as soon as it's available","","42847922MDD3003 [Prod]","CZE","All","All","All","None","All","All","All","All","Open,Answered","No","None","None","None","None","None","Final","With Text","5.6 - 1.0","5/20/2026 1:21:29 PM","GMT","Vladimir Buzalka [Site Manager]","","20260519 09:28:33.810","","","2","4","4","1","2","4","4"
|
||||
"CZE","S10-CZ10012","Neuropsychiatrie Petrska sro","CZ100120005","Early Terminated","Investigator Periodic Review Acknowledgments/ Interim Signatures ","Periodic Investigator's EDC Review Acknowledgement","1/0/N","","As the investigator, I have ensured the accuracy, completeness, and timeliness of the data reported to the sponsor in the CRFs.","Auto","20876096","Open","systemuser","17 May 2026 08:22:12:287","","","","","3","","","NOTIFICATION: Date of discontinuation/completion has been completed for the subject. To ensure compliance with regulatory requirements for data oversight, please mark the tick box after you have reviewed the subject’s data in EDC.","","42847922MDD3003 [Prod]","CZE","All","All","All","None","All","All","All","All","Open,Answered","No","None","None","None","None","None","Final","With Text","5.6 - 1.0","5/20/2026 1:21:29 PM","GMT","Vladimir Buzalka [Site Manager]","","20260517 08:22:12.287","","","1","4","4","1","1","4","4"
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"study": "42847922MDD3003",
|
||||
"report_id": 92,
|
||||
"forms": [
|
||||
"Acknowledgement Reporting Form",
|
||||
"Acknowledgement Upload Form",
|
||||
"Adverse Event of Special Interest",
|
||||
"Adverse Events/Serious Aes",
|
||||
"Alcohol Test",
|
||||
"Arizona Sexual Experiences Scale Summary",
|
||||
"Arizona Sexual Experiences Scale-Female",
|
||||
"Arizona Sexual Experiences Scale-Male",
|
||||
"Change in Background Antidepressant",
|
||||
"Clinical Global Impression - S (Depression)",
|
||||
"Clinical Outcome Assessments Completion Status",
|
||||
"Columbia-Suicide Severity Rating Scale - Baseline/Screening Version",
|
||||
"Columbia-Suicide Severity Rating Scale - Since Last Visit",
|
||||
"Comments",
|
||||
"Concomitant Therapy / Medication",
|
||||
"Date of Visit",
|
||||
"Death Information",
|
||||
"Demographics",
|
||||
"Derivation (operational form)",
|
||||
"Drug Expiry Information",
|
||||
"Drug Testing",
|
||||
"DSL Index Page (Must have for DSL functionality; not visible to the sites) (operational form)",
|
||||
"DUMMY (operational form)",
|
||||
"Educational Level",
|
||||
"Enrollment",
|
||||
"EQ-5D-5L",
|
||||
"Evaluation of Response - Induction Phase",
|
||||
"Evaluation of Response - Stabilization Phase",
|
||||
"General Medical History",
|
||||
"Inclusion/Exclusion Criteria",
|
||||
"Insomnia Severity Index",
|
||||
"Insomnia Severity Index (Clinician Version)",
|
||||
"Integrated Medication Kit Accountability Information Double Blind Maintenance",
|
||||
"Integrated Medication Kit Accountability Information Double Blind Part 1",
|
||||
"Integrated Medication Kit Accountability Information Open Label Part 2",
|
||||
"Interim Investigator Signature",
|
||||
"IRT Stratification",
|
||||
"Local Chemistry (Unscheduled)",
|
||||
"Local Hematology (Unscheduled)",
|
||||
"Local Labs for Background Antidepressant Compliance",
|
||||
"Menstrual Cycle Tracking",
|
||||
"Menstrual Cycle Tracking Log",
|
||||
"MGH ATRQ, Geriatric, Section I and II",
|
||||
"MGH ATRQ, Non-Geriatric, Section I and II",
|
||||
"MGH-ATRQ - Summary",
|
||||
"MGH-ATRQ - Therapy Questions",
|
||||
"Mini-Mental State Examination",
|
||||
"Neurologic Examination",
|
||||
"Patient Global Impression of Change - Depression/Insomnia",
|
||||
"Patient Global Impression of Severity - Insomnia",
|
||||
"Patient Health Questionnaire - 9 Item",
|
||||
"Perceived Treatment Group Assignment",
|
||||
"Periodic Investigator's EDC Review Acknowledgement",
|
||||
"Physical Examination",
|
||||
"Physician Withdrawal Checklist (PWC)",
|
||||
"Pregnancy Test",
|
||||
"Preplanned Surgeries/Procedures",
|
||||
"Procedures",
|
||||
"PROMIS - Sleep Disturbance",
|
||||
"Protocol Amendment Implementation (Operational Form)",
|
||||
"Psychiatric History for Major Depressive Disorder",
|
||||
"Psychotherapy",
|
||||
"Psychotherapy v2.0",
|
||||
"Randomization",
|
||||
"Relapse - MAJOR DEPRESSIVE DISORDER",
|
||||
"Relapse Criteria",
|
||||
"Relapse Criteria Unscheduled",
|
||||
"Relevant Additional Drug Therapies",
|
||||
"Relevant information Selection",
|
||||
"Relevant Local Laboratory",
|
||||
"Relevant Local Laboratory Data",
|
||||
"Relevant Medical History",
|
||||
"Relevant Preplanned Surgeries/Procedures",
|
||||
"Relevant Procedures",
|
||||
"Relevant Study Medication",
|
||||
"Relevant Tests",
|
||||
"Safety Report Form",
|
||||
"SCID-CT",
|
||||
"SCID-CT Insomnia Disorder Supplemental",
|
||||
"Sheehan Disability Scale",
|
||||
"SIGH-D-17",
|
||||
"Site Independent Qualification Assessment",
|
||||
"Site/Invest Identification",
|
||||
"Structured Interview Guide for the Montgomery-Asberg Depression Rating Scale",
|
||||
"Study Drug Administration Double Blind Maintenance",
|
||||
"Study Drug Administration Double Blind Part 1",
|
||||
"Study Drug Administration Open Label Part 2 - Induction",
|
||||
"Study Drug Administration Open Label Part 2 - Stabilization",
|
||||
"Subject",
|
||||
"Subject Site Switch",
|
||||
"Treatment Disposition",
|
||||
"Treatment Unblinding",
|
||||
"Trial Disposition Completion/ Discontinuation",
|
||||
"Unscheduled Assessments",
|
||||
"Unsuccessful Contact Attempts",
|
||||
"Vital Signs",
|
||||
"Vital Signs (Unscheduled)"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"study": "77242113UCO3001",
|
||||
"report_id": 92,
|
||||
"forms": [
|
||||
"Acknowledgement Reporting Form",
|
||||
"Acknowledgement Upload Form",
|
||||
"Additional Liver Event Assessment Forms",
|
||||
"Advanced Therapy Treatment Failure Reason",
|
||||
"Adverse Events/Serious AEs",
|
||||
"Alcohol Consumption",
|
||||
"Axial Spondyloarthropathy Diagnosis Information",
|
||||
"Clinical Outcome Assessments Completion Status",
|
||||
"Concomitant Therapy",
|
||||
"Consents / Withdrawal of Consents for Optional Research",
|
||||
"Corticosteroid, Immunomodulator And Oral Aminosalicylates History",
|
||||
"Date of Visit",
|
||||
"Death Information",
|
||||
"Demographics",
|
||||
"Derivation (operational form)",
|
||||
"DSL Index Page (Must have for DSL functionality; not visible to the sites) (operational form)",
|
||||
"DUMMY (operational form)",
|
||||
"Endoscopy Information",
|
||||
"Enrollment",
|
||||
"Family History specific to Hepatic Event",
|
||||
"Food/Liquid Fasting Compliance",
|
||||
"General Medical History",
|
||||
"GI Related Surgeries and Procedures",
|
||||
"Group Selection",
|
||||
"Hepatic Event - Other Risk Factors",
|
||||
"History of GI Past Related Surgeries/Procedures",
|
||||
"Inclusion/Exclusion Criteria",
|
||||
"Integrated Medication Kit Accountability Information",
|
||||
"Interim Investigator Signature",
|
||||
"Intestinal Ultrasound",
|
||||
"Limitation on Retention of Samples",
|
||||
"Liver Biopsy",
|
||||
"Liver Chemistry Abnormalities Assessment Form",
|
||||
"Liver Event Case of AEs",
|
||||
"Liver Event Chemistry Analytes",
|
||||
"Liver Event Level 1 Analytes",
|
||||
"Liver Event Level 2 Analytes",
|
||||
"Liver Event Workup Completion Status",
|
||||
"Liver Imaging Assessment",
|
||||
"Liver-related Signs and Symptoms of Hypersensitivity",
|
||||
"Liver-related Signs and Symptoms of Liver Injury",
|
||||
"Medical Encounters",
|
||||
"Medical History: Liver-related Diseases",
|
||||
"Periodic Investigator's EDC Review Acknowledgement",
|
||||
"Pharmacokinetics, Pharmacodynamic and Biomarker Sample Collection",
|
||||
"Preplanned Surgeries/Procedures",
|
||||
"Protocol Amendment Implementation (Operational Form)",
|
||||
"Randomization",
|
||||
"Relevant Additional Drug Therapies",
|
||||
"Relevant Information Selection",
|
||||
"Relevant Local Laboratory",
|
||||
"Relevant Local Laboratory Data",
|
||||
"Relevant Procedures",
|
||||
"Relevant Study Medication",
|
||||
"Relevant Tests",
|
||||
"Safety Report Form",
|
||||
"Screening for Tuberculosis",
|
||||
"Site/Invest Identification",
|
||||
"Study Drug Administration",
|
||||
"Subject",
|
||||
"Subject Site Switch",
|
||||
"Substance Use Alcohol",
|
||||
"Substance Use Tobacco/Nicotine",
|
||||
"Treatment Disposition (End of treatment)",
|
||||
"Treatment Unblinding",
|
||||
"Trial Disposition (Completion / Discontinuation)",
|
||||
"Tuberculosis Testing and Results",
|
||||
"Ulcerative Colitis Disease History",
|
||||
"Ulcerative Colitis Medication History",
|
||||
"Unscheduled Assessments",
|
||||
"Vital Signs"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user