z230
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"janssen-mongo": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["U:\\PythonProject\\Janssen\\mcp_mongo.py"],
|
||||||
|
"cwd": "U:\\PythonProject\\Janssen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"Protocol","Study Population","Country","Site","Principal Investigator","Participant ID","Baseline Stool Frequency","Visit","Visit Date","Endoscopy Completed?","Endoscopy Date","Bowel Preparation Start Date 1","Bowel Preparation End Date 1","Bowel Preparation Start Date 2","Bowel Preparation End Date 2","Central Endoscopy Score","Local Endoscopy Score","PGA Score","Eligible Day (-1)","Day (-1) Excluded Reason(s)","Eligible Day (-2)","Day (-2) Excluded Reason(s)","Eligible Day (-3)","Day (-3) Excluded Reason(s)","Eligible Day (-4)","Day (-4) Excluded Reason(s)","Eligible Day (-5)","Day (-5) Excluded Reason(s)","Eligible Day (-6)","Day (-6) Excluded Reason(s)","Eligible Day (-7)","Day (-7) Excluded Reason(s)","Eligible Day (-8)","Day (-8) Excluded Reason(s)","Eligible Day (-9)","Day (-9) Excluded Reason(s)","Eligible Day (-10)","Day (-10) Excluded Reason(s)","Eligible Day (-1) Stool Count","Eligible Day (-2) Stool Count","Eligible Day (-3) Stool Count","Eligible Day (-4) Stool Count","Eligible Day (-5) Stool Count","Eligible Day (-6) Stool Count","Eligible Day (-7) Stool Count","Eligible Day (-8) Stool Count","Eligible Day (-9) Stool Count","Eligible Day (-10) Stool Count","Stool Frequency Sub-score","Eligible Day (-1) Rectal Bleeding Score","Eligible Day (-2) Rectal Bleeding Score","Eligible Day (-3) Rectal Bleeding Score","Eligible Day (-4) Rectal Bleeding Score","Eligible Day (-5) Rectal Bleeding Score","Eligible Day (-6) Rectal Bleeding Score","Eligible Day (-7) Rectal Bleeding Score","Eligible Day (-8) Rectal Bleeding Score","Eligible Day (-9) Rectal Bleeding Score","Eligible Day (-10) Rectal Bleeding Score","Rectal Bleeding Sub-score","Partial Mayo Score","Modified Mayo Score","Full Mayo Score","Site Action","Last Mayo Score Submission","Week I-12 Clinical Responder","Week I-12 Clinical Remission","Clinical Flare","Loss of Response","Partial Mayo Response Post Loss of Response","Partial Mayo Response for Clinical Non-Responders"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-0","19 Feb 2026","Yes","05 Feb 2026","04 Feb 2026","04 Feb 2026","-","-","2","-","3","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Day Not Applicable for Calculation","10 Feb 2026","Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","10","8","7","5","7","8","8","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","7","6","9","-","08 Apr 2026 07:11:25","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-2","04 Mar 2026","-","-","-","-","-","-","-","-","3","03 Mar 2026","-","02 Mar 2026","-","01 Mar 2026","-","28 Feb 2026","-","27 Feb 2026","-","26 Feb 2026","-","25 Feb 2026","-","24 Feb 2026","Day Not Applicable for Calculation","23 Feb 2026","Day Not Applicable for Calculation","22 Feb 2026","Day Not Applicable for Calculation","5","4","5","4","5","6","6","-","-","-","2","1","0","1","0","1","0","1","-","-","-","1","6","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-4","18 Mar 2026","-","-","-","-","-","-","-","-","2","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","-","11 Mar 2026","-","10 Mar 2026","Day Not Applicable for Calculation","09 Mar 2026","Day Not Applicable for Calculation","08 Mar 2026","Day Not Applicable for Calculation","5","5","5","4","5","4","5","-","-","-","2","1","0","0","1","1","1","0","-","-","-","1","5","","","-","08 Apr 2026 07:11:43","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-8","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","4","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-12","13 May 2026","Yes","06 May 2026","05 May 2026","05 May 2026","-","-","1","-","1","12 May 2026","-","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","Endoscopy","05 May 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","04 May 2026","-","03 May 2026","Day Not Applicable for Calculation","5","4","6","5","5","5","-","-","3","-","2","1","0","1","1","1","1","-","-","1","-","1","4","4","5","-","-","Clinical Responder","No","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-0","08 Apr 2026","Yes","18 Mar 2026","17 Mar 2026","18 Mar 2026","-","-","2","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","Missing Diary","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","-","3","3","4","-","-","-","1","0","0","0","-","0","0","1","-","-","-","0","3","3","5","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-2","23 Apr 2026","-","-","-","-","-","-","-","-","2","22 Apr 2026","Missing Diary","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","Day Not Applicable for Calculation","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","-","3","3","6","5","5","4","-","-","-","2","-","0","0","1","1","1","1","-","-","-","1","5","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-4","06 May 2026","-","-","-","-","-","-","-","-","1","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","6","3","2","3","3","3","3","-","-","-","1","1","0","0","0","1","1","0","-","-","-","0","2","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","1","I-0","27 May 2026","Yes","13 May 2026","12 May 2026","12 May 2026","-","-","3","-","2","26 May 2026","-","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","Day Not Applicable for Calculation","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","6","9","7","8","9","7","8","-","-","-","3","2","2","2","2","1","1","1","-","-","-","2","7","8","10","-","27 May 2026 07:24:39","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-0","20 Mar 2026","Yes","19 Feb 2026","-","-","-","-","3","-","3","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","Day Not Applicable for Calculation","11 Mar 2026","Day Not Applicable for Calculation","10 Mar 2026","Day Not Applicable for Calculation","7","7","8","8","7","8","5","-","-","-","3","2","1","1","1","1","1","0","-","-","-","1","7","7","10","-","20 Mar 2026 07:02:44","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","Medication For Diarrhea","06 Apr 2026","Medication For Diarrhea","05 Apr 2026","Medication For Diarrhea","04 Apr 2026","Medication For Diarrhea","03 Apr 2026","Medication For Diarrhea","02 Apr 2026","Medication For Diarrhea","01 Apr 2026","Medication For Diarrhea","31 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","30 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","Non-Evaluable","Non-Evaluable","Non-Evaluable","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-4","15 Apr 2026","-","-","-","-","-","-","-","-","3","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","-","11 Apr 2026","-","10 Apr 2026","-","09 Apr 2026","-","08 Apr 2026","-","07 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","06 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","05 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","9","22","20","19","17","18","18","-","-","-","3","1","3","2","2","2","2","2","-","-","-","2","8","","","-","04 May 2026 22:06:47","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-8","18 May 2026","-","-","-","-","-","-","-","-","2","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","7","5","9","7","7","8","8","-","-","-","3","1","1","1","1","1","1","1","-","-","-","1","6","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","1","I-0","26 May 2026","Yes","14 May 2026","13 May 2026","13 May 2026","-","-","2","-","2","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","8","8","6","7","7","6","7","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","7","7","9","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-0","05 May 2026","Yes","24 Apr 2026","23 Apr 2026","23 Apr 2026","-","-","2","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","5","5","5","5","5","5","5","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","5","5","7","-","05 May 2026 11:19:40","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-2","19 May 2026","-","-","-","-","-","-","-","-","1","18 May 2026","-","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","Day Not Applicable for Calculation","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","5","4","5","5","5","4","6","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","19 May 2026 10:38:25","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-0","07 Apr 2026","Yes","24 Mar 2026","22 Mar 2026","22 Mar 2026","-","-","2","-","2","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","8","11","5","9","11","10","13","-","-","-","3","1","2","2","2","2","2","2","-","-","-","2","7","7","9","-","04 May 2026 08:44:52","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","7","5","6","6","7","8","2","-","-","-","1","1","0","1","1","1","2","0","-","-","-","1","4","","","-","04 May 2026 08:45:07","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-4","07 May 2026","-","-","-","-","-","-","-","-","1","06 May 2026","-","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","Day Not Applicable for Calculation","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","8","7","7","8","4","11","7","-","-","-","1","2","1","1","1","0","1","1","-","-","-","1","3","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-0","24 Mar 2026","Yes","12 Mar 2026","11 Mar 2026","11 Mar 2026","-","-","2","-","2","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","Day Not Applicable for Calculation","15 Mar 2026","Day Not Applicable for Calculation","14 Mar 2026","Day Not Applicable for Calculation","8","6","5","7","6","7","6","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","6","6","8","-","05 Apr 2026 22:41:27","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","5","2","3","6","5","5","5","-","-","-","2","0","0","0","0","1","1","0","-","-","-","0","4","","","-","27 May 2026 12:53:52","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-4","21 Apr 2026","-","-","-","-","-","-","-","-","0","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","4","3","4","3","3","4","4","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","2","","","-","27 May 2026 12:54:41","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-0","12 May 2026","Yes","21 Apr 2026","20 Apr 2026","21 Apr 2026","-","-","2","-","2","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","-","05 May 2026","Missing Diary","04 May 2026","Day Not Applicable for Calculation","03 May 2026","Day Not Applicable for Calculation","02 May 2026","Day Not Applicable for Calculation","2","1","1","1","1","2","-","-","-","-","0","0","0","0","0","0","0","-","-","-","-","0","2","2","4","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-2","26 May 2026","-","-","-","-","-","-","-","-","1","25 May 2026","-","24 May 2026","Missing Diary","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Missing Diary;Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","1","-","1","2","1","2","2","-","-","-","1","0","-","0","0","0","0","0","-","-","-","0","2","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","Unscheduled 1","04 May 2026","Yes","20 Apr 2026","12 Apr 2026","15 Apr 2026","-","-","2","-","3","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Day Not Applicable for Calculation","5","6","6","7","6","3","3","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","5","4","7","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","I-0","18 May 2026","Yes","01 May 2026","01 May 2026","01 May 2026","-","-","2","-","3","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","6","6","6","6","6","6","6","-","-","-","3","0","0","0","0","0","0","0","-","-","-","0","6","5","8","-","04 May 2026 09:51:14","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-0","07 Apr 2026","Yes","16 Mar 2026","15 Mar 2026","16 Mar 2026","-","-","3","-","3","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","11","11","10","11","11","10","9","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","8","8","11","-","20 Apr 2026 09:27:58","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-2","20 Apr 2026","-","-","-","-","-","-","-","-","3","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","10 Apr 2026","Day Not Applicable for Calculation","8","7","9","8","8","7","8","-","-","-","3","2","2","1","1","1","2","1","-","-","-","1","7","","","-","20 Apr 2026 09:29:01","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","6","6","6","6","7","7","6","-","-","-","3","0","0","1","1","1","1","1","-","-","-","1","5","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","1","I-0","19 Feb 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Endoscopy;Bowel Preparation for Procedure;Day Not Applicable for Calculation","10 Feb 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","3","2","2","3","4","3","2","-","-","-","1","1","1","0","0","0","2","2","-","-","-","1","4","4","6","-","19 Feb 2026 15:41:35","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-0","09 Mar 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","08 Mar 2026","-","07 Mar 2026","-","06 Mar 2026","-","05 Mar 2026","-","04 Mar 2026","-","03 Mar 2026","Missing Diary","02 Mar 2026","Missing Diary","01 Mar 2026","Missing Diary;Day Not Applicable for Calculation","28 Feb 2026","Missing Diary;Day Not Applicable for Calculation","27 Feb 2026","Missing Diary;Day Not Applicable for Calculation","7","7","6","6","7","-","-","-","-","-","3","2","2","2","2","2","-","-","-","-","-","2","7","7","9","-","22 Mar 2026 18:34:58","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-2","27 Mar 2026","-","-","-","-","-","-","-","-","2","26 Mar 2026","-","25 Mar 2026","-","24 Mar 2026","-","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","Day Not Applicable for Calculation","18 Mar 2026","Day Not Applicable for Calculation","17 Mar 2026","Day Not Applicable for Calculation","7","3","3","3","5","5","5","-","-","-","2","0","0","1","1","1","1","2","-","-","-","1","5","","","-","27 Mar 2026 07:22:31","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-4","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","3","-","-","-","2","1","0","0","2","1","1","2","-","-","-","1","5","","","-","08 Apr 2026 07:59:35","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-8","04 May 2026","-","-","-","-","-","-","-","-","2","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Missing Diary;Day Not Applicable for Calculation","3","5","3","3","3","2","3","-","-","-","1","0","0","0","0","0","0","0","-","-","-","0","3","","","-","04 May 2026 07:52:47","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-0","09 Apr 2026","Yes","08 Apr 2026","31 Mar 2026","01 Apr 2026","-","-","2","-","2","08 Apr 2026","Endoscopy","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","31 Mar 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","30 Mar 2026","-","-","3","3","4","3","4","3","-","-","3","1","-","2","2","2","2","2","2","-","-","2","2","5","5","7","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","3","3","5","3","2","3","2","-","-","-","1","1","2","2","1","1","1","2","-","-","-","1","4","","","-","22 Apr 2026 14:03:33","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","4","2","2","2","2","2","2","-","-","-","1","1","1","1","1","2","1","1","-","-","-","1","4","","","-","05 May 2026 07:30:02","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
Protocol,Study Population,Country,Site,Principal Investigator,Participant ID,Baseline Stool Frequency,Visit,Visit Date,Endoscopy Completed?,Endoscopy Date,Bowel Preparation Start Date 1,Bowel Preparation End Date 1,Bowel Preparation Start Date 2,Bowel Preparation End Date 2,Central Endoscopy Score,Local Endoscopy Score,PGA Score,Eligible Day (-1),Day (-1) Excluded Reason(s),Eligible Day (-2),Day (-2) Excluded Reason(s),Eligible Day (-3),Day (-3) Excluded Reason(s),Eligible Day (-4),Day (-4) Excluded Reason(s),Eligible Day (-5),Day (-5) Excluded Reason(s),Eligible Day (-6),Day (-6) Excluded Reason(s),Eligible Day (-7),Day (-7) Excluded Reason(s),Eligible Day (-8),Day (-8) Excluded Reason(s),Eligible Day (-9),Day (-9) Excluded Reason(s),Eligible Day (-10),Day (-10) Excluded Reason(s),Eligible Day (-1) Stool Count,Eligible Day (-2) Stool Count,Eligible Day (-3) Stool Count,Eligible Day (-4) Stool Count,Eligible Day (-5) Stool Count,Eligible Day (-6) Stool Count,Eligible Day (-7) Stool Count,Eligible Day (-8) Stool Count,Eligible Day (-9) Stool Count,Eligible Day (-10) Stool Count,Stool Frequency Sub-score,Eligible Day (-1) Rectal Bleeding Score,Eligible Day (-2) Rectal Bleeding Score,Eligible Day (-3) Rectal Bleeding Score,Eligible Day (-4) Rectal Bleeding Score,Eligible Day (-5) Rectal Bleeding Score,Eligible Day (-6) Rectal Bleeding Score,Eligible Day (-7) Rectal Bleeding Score,Eligible Day (-8) Rectal Bleeding Score,Eligible Day (-9) Rectal Bleeding Score,Eligible Day (-10) Rectal Bleeding Score,Rectal Bleeding Sub-score,Partial Mayo Score,Modified Mayo Score,Full Mayo Score,Site Action,Last Mayo Score Submission,Week I-12 Clinical Responder,Week I-12 Clinical Remission,Clinical Flare,Loss of Response,Partial Mayo Response Post Loss of Response,Partial Mayo Response for Clinical Non-Responders
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012001,1,I-0,19 Feb 2026,Yes,05 Feb 2026,04 Feb 2026,04 Feb 2026,-,-,2,-,3,18 Feb 2026,-,17 Feb 2026,-,16 Feb 2026,-,15 Feb 2026,-,14 Feb 2026,-,13 Feb 2026,-,12 Feb 2026,-,11 Feb 2026,Day Not Applicable for Calculation,10 Feb 2026,Day Not Applicable for Calculation,09 Feb 2026,Day Not Applicable for Calculation,10,8,7,5,7,8,8,-,-,-,3,1,1,1,0,1,1,1,-,-,-,1,7,6,10,-,08 Apr 2026 07:11:25,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012001,1,I-2,04 Mar 2026,-,-,-,-,-,-,-,-,3,03 Mar 2026,-,02 Mar 2026,-,01 Mar 2026,-,28 Feb 2026,-,27 Feb 2026,-,26 Feb 2026,-,25 Feb 2026,-,24 Feb 2026,Day Not Applicable for Calculation,23 Feb 2026,Day Not Applicable for Calculation,22 Feb 2026,Day Not Applicable for Calculation,5,4,5,4,5,6,6,-,-,-,2,1,0,1,0,1,0,1,-,-,-,1,6,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012001,1,I-4,18 Mar 2026,-,-,-,-,-,-,-,-,2,17 Mar 2026,-,16 Mar 2026,-,15 Mar 2026,-,14 Mar 2026,-,13 Mar 2026,-,12 Mar 2026,-,11 Mar 2026,-,10 Mar 2026,Day Not Applicable for Calculation,09 Mar 2026,Day Not Applicable for Calculation,08 Mar 2026,Day Not Applicable for Calculation,5,5,5,4,5,4,5,-,-,-,2,1,0,0,1,1,1,0,-,-,-,1,5,,,-,08 Apr 2026 07:11:43,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012001,1,I-8,05 May 2026,-,-,-,-,-,-,-,-,1,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,Day Not Applicable for Calculation,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,3,3,4,4,5,4,4,-,-,-,2,1,1,1,1,1,1,1,-,-,-,1,4,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012001,1,I-12,13 May 2026,Yes,06 May 2026,05 May 2026,05 May 2026,-,-,1,-,1,12 May 2026,-,11 May 2026,-,10 May 2026,-,09 May 2026,-,08 May 2026,-,07 May 2026,-,06 May 2026,Endoscopy,05 May 2026,Bowel Preparation for Procedure;Day Not Applicable for Calculation,04 May 2026,-,03 May 2026,Day Not Applicable for Calculation,5,4,6,5,5,5,-,-,3,-,2,1,0,1,1,1,1,-,-,1,-,1,4,4,5,-,-,Clinical Responder,No,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012002,1,I-0,08 Apr 2026,Yes,18 Mar 2026,17 Mar 2026,18 Mar 2026,-,-,2,-,2,07 Apr 2026,-,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,Missing Diary,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,-,31 Mar 2026,Day Not Applicable for Calculation,30 Mar 2026,Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,3,3,4,-,3,3,4,-,-,-,1,0,0,0,-,0,0,1,-,-,-,0,3,3,5,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012002,1,I-2,23 Apr 2026,-,-,-,-,-,-,-,-,2,22 Apr 2026,Missing Diary,21 Apr 2026,-,20 Apr 2026,-,19 Apr 2026,-,18 Apr 2026,-,17 Apr 2026,-,16 Apr 2026,-,15 Apr 2026,Day Not Applicable for Calculation,14 Apr 2026,Day Not Applicable for Calculation,13 Apr 2026,Day Not Applicable for Calculation,-,3,3,6,5,5,4,-,-,-,2,-,0,0,1,1,1,1,-,-,-,1,5,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012002,1,I-4,06 May 2026,-,-,-,-,-,-,-,-,1,05 May 2026,-,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,Day Not Applicable for Calculation,27 Apr 2026,Day Not Applicable for Calculation,26 Apr 2026,Day Not Applicable for Calculation,6,3,2,3,3,3,3,-,-,-,1,1,0,0,0,1,1,0,-,-,-,0,2,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10001,Matej Falc,CZ100012003,1,I-0,27 May 2026,Yes,13 May 2026,12 May 2026,12 May 2026,-,-,3,-,2,26 May 2026,-,25 May 2026,-,24 May 2026,-,23 May 2026,-,22 May 2026,-,21 May 2026,-,20 May 2026,-,19 May 2026,Day Not Applicable for Calculation,18 May 2026,Day Not Applicable for Calculation,17 May 2026,Day Not Applicable for Calculation,6,9,7,8,9,7,8,-,-,-,3,2,2,2,2,1,1,1,-,-,-,2,7,8,10,-,27 May 2026 07:24:39,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10006,Michal Konecny,CZ100062001,1,I-0,20 Mar 2026,Yes,19 Feb 2026,-,-,-,-,3,-,3,19 Mar 2026,-,18 Mar 2026,-,17 Mar 2026,-,16 Mar 2026,-,15 Mar 2026,-,14 Mar 2026,-,13 Mar 2026,-,12 Mar 2026,Day Not Applicable for Calculation,11 Mar 2026,Day Not Applicable for Calculation,10 Mar 2026,Day Not Applicable for Calculation,7,7,8,8,7,8,5,-,-,-,3,2,1,1,1,1,1,0,-,-,-,1,7,7,10,-,20 Mar 2026 07:02:44,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10006,Michal Konecny,CZ100062001,1,I-2,08 Apr 2026,-,-,-,-,-,-,-,-,2,07 Apr 2026,Medication For Diarrhea,06 Apr 2026,Medication For Diarrhea,05 Apr 2026,Medication For Diarrhea,04 Apr 2026,Medication For Diarrhea,03 Apr 2026,Medication For Diarrhea,02 Apr 2026,Medication For Diarrhea,01 Apr 2026,Medication For Diarrhea,31 Mar 2026,Medication For Diarrhea;Day Not Applicable for Calculation,30 Mar 2026,Medication For Diarrhea;Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,-,-,-,-,-,-,-,-,-,-,Non-Evaluable,-,-,-,-,-,-,-,-,-,-,Non-Evaluable,Non-Evaluable,Non-Evaluable,Non-Evaluable,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10006,Michal Konecny,CZ100062001,1,I-4,15 Apr 2026,-,-,-,-,-,-,-,-,3,14 Apr 2026,-,13 Apr 2026,-,12 Apr 2026,-,11 Apr 2026,-,10 Apr 2026,-,09 Apr 2026,-,08 Apr 2026,-,07 Apr 2026,Medication For Diarrhea;Day Not Applicable for Calculation,06 Apr 2026,Medication For Diarrhea;Day Not Applicable for Calculation,05 Apr 2026,Medication For Diarrhea;Day Not Applicable for Calculation,9,22,20,19,17,18,18,-,-,-,3,1,3,2,2,2,2,2,-,-,-,2,8,,,-,04 May 2026 22:06:47,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10006,Michal Konecny,CZ100062001,1,I-8,18 May 2026,-,-,-,-,-,-,-,-,2,17 May 2026,-,16 May 2026,-,15 May 2026,-,14 May 2026,-,13 May 2026,-,12 May 2026,-,11 May 2026,-,10 May 2026,Day Not Applicable for Calculation,09 May 2026,Day Not Applicable for Calculation,08 May 2026,Day Not Applicable for Calculation,7,5,9,7,7,8,8,-,-,-,3,1,1,1,1,1,1,1,-,-,-,1,6,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10006,Michal Konecny,CZ100062002,1,I-0,26 May 2026,Yes,14 May 2026,13 May 2026,13 May 2026,-,-,2,-,2,25 May 2026,-,24 May 2026,-,23 May 2026,-,22 May 2026,-,21 May 2026,-,20 May 2026,-,19 May 2026,-,18 May 2026,Day Not Applicable for Calculation,17 May 2026,Day Not Applicable for Calculation,16 May 2026,Day Not Applicable for Calculation,8,8,6,7,7,6,7,-,-,-,3,2,2,2,2,2,2,2,-,-,-,2,7,7,9,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10009,Jiri Pumprla,CZ100092001,1,I-0,05 May 2026,Yes,24 Apr 2026,23 Apr 2026,23 Apr 2026,-,-,2,-,2,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,Day Not Applicable for Calculation,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,5,5,5,5,5,5,5,-,-,-,2,1,1,1,1,1,1,1,-,-,-,1,5,5,7,-,05 May 2026 11:19:40,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10009,Jiri Pumprla,CZ100092001,1,I-2,19 May 2026,-,-,-,-,-,-,-,-,1,18 May 2026,-,17 May 2026,-,16 May 2026,-,15 May 2026,-,14 May 2026,-,13 May 2026,-,12 May 2026,-,11 May 2026,Day Not Applicable for Calculation,10 May 2026,Day Not Applicable for Calculation,09 May 2026,Day Not Applicable for Calculation,5,4,5,5,5,4,6,-,-,-,2,1,1,1,1,1,1,1,-,-,-,1,4,,,-,19 May 2026 10:38:25,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10012,Stefan Konecny,CZ100122001,5,I-0,07 Apr 2026,Yes,24 Mar 2026,22 Mar 2026,22 Mar 2026,-,-,2,-,2,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,-,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,-,31 Mar 2026,-,30 Mar 2026,Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,28 Mar 2026,Day Not Applicable for Calculation,8,11,5,9,11,10,13,-,-,-,3,1,2,2,2,2,2,2,-,-,-,2,7,7,9,-,04 May 2026 08:44:52,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10012,Stefan Konecny,CZ100122001,5,I-2,22 Apr 2026,-,-,-,-,-,-,-,-,2,21 Apr 2026,-,20 Apr 2026,-,19 Apr 2026,-,18 Apr 2026,-,17 Apr 2026,-,16 Apr 2026,-,15 Apr 2026,-,14 Apr 2026,Day Not Applicable for Calculation,13 Apr 2026,Day Not Applicable for Calculation,12 Apr 2026,Day Not Applicable for Calculation,7,5,6,6,7,8,2,-,-,-,1,1,0,1,1,1,2,0,-,-,-,1,4,,,-,04 May 2026 08:45:07,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10012,Stefan Konecny,CZ100122001,5,I-4,07 May 2026,-,-,-,-,-,-,-,-,1,06 May 2026,-,05 May 2026,-,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,Day Not Applicable for Calculation,28 Apr 2026,Day Not Applicable for Calculation,27 Apr 2026,Day Not Applicable for Calculation,8,7,7,8,4,11,7,-,-,-,1,2,1,1,1,0,1,1,-,-,-,1,3,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10013,David Stepek,CZ100132001,1,I-0,24 Mar 2026,Yes,12 Mar 2026,11 Mar 2026,11 Mar 2026,-,-,2,-,2,23 Mar 2026,-,22 Mar 2026,-,21 Mar 2026,-,20 Mar 2026,-,19 Mar 2026,-,18 Mar 2026,-,17 Mar 2026,-,16 Mar 2026,Day Not Applicable for Calculation,15 Mar 2026,Day Not Applicable for Calculation,14 Mar 2026,Day Not Applicable for Calculation,8,6,5,7,6,7,6,-,-,-,3,1,1,1,0,1,1,1,-,-,-,1,6,6,8,-,05 Apr 2026 22:41:27,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10013,David Stepek,CZ100132001,1,I-2,08 Apr 2026,-,-,-,-,-,-,-,-,2,07 Apr 2026,-,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,-,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,-,31 Mar 2026,Day Not Applicable for Calculation,30 Mar 2026,Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,5,2,3,6,5,5,5,-,-,-,2,0,0,0,0,1,1,0,-,-,-,0,4,,,-,27 May 2026 12:53:52,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10013,David Stepek,CZ100132001,1,I-4,21 Apr 2026,-,-,-,-,-,-,-,-,0,20 Apr 2026,-,19 Apr 2026,-,18 Apr 2026,-,17 Apr 2026,-,16 Apr 2026,-,15 Apr 2026,-,14 Apr 2026,-,13 Apr 2026,Day Not Applicable for Calculation,12 Apr 2026,Day Not Applicable for Calculation,11 Apr 2026,Day Not Applicable for Calculation,4,3,4,3,3,4,4,-,-,-,2,0,0,0,0,0,0,0,-,-,-,0,2,,,-,27 May 2026 12:54:41,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10013,David Stepek,CZ100132002,1,I-0,12 May 2026,Yes,21 Apr 2026,20 Apr 2026,21 Apr 2026,-,-,2,-,2,11 May 2026,-,10 May 2026,-,09 May 2026,-,08 May 2026,-,07 May 2026,-,06 May 2026,-,05 May 2026,Missing Diary,04 May 2026,Day Not Applicable for Calculation,03 May 2026,Day Not Applicable for Calculation,02 May 2026,Day Not Applicable for Calculation,2,1,1,1,1,2,-,-,-,-,0,0,0,0,0,0,0,-,-,-,-,0,2,2,4,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10013,David Stepek,CZ100132002,1,I-2,26 May 2026,-,-,-,-,-,-,-,-,1,25 May 2026,-,24 May 2026,Missing Diary,23 May 2026,-,22 May 2026,-,21 May 2026,-,20 May 2026,-,19 May 2026,-,18 May 2026,Missing Diary;Day Not Applicable for Calculation,17 May 2026,Day Not Applicable for Calculation,16 May 2026,Day Not Applicable for Calculation,1,-,1,2,1,2,2,-,-,-,1,0,-,0,0,0,0,0,-,-,-,0,2,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adolescent,Czech Republic,DD5-CZ10020,Lucie Gonsorcikova,CZ100201001,1,Unscheduled 1,04 May 2026,Yes,20 Apr 2026,12 Apr 2026,15 Apr 2026,-,-,2,-,3,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,-,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,24 Apr 2026,Day Not Applicable for Calculation,5,6,6,7,6,3,3,-,-,-,2,0,0,0,0,0,0,0,-,-,-,0,5,4,7,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adolescent,Czech Republic,DD5-CZ10020,Lucie Gonsorcikova,CZ100201001,1,I-0,18 May 2026,Yes,01 May 2026,01 May 2026,01 May 2026,-,-,2,-,3,17 May 2026,-,16 May 2026,-,15 May 2026,-,14 May 2026,-,13 May 2026,-,12 May 2026,-,11 May 2026,-,10 May 2026,Day Not Applicable for Calculation,09 May 2026,Day Not Applicable for Calculation,08 May 2026,Day Not Applicable for Calculation,6,6,6,6,6,6,6,-,-,-,3,0,0,0,0,0,0,0,-,-,-,0,6,5,8,-,04 May 2026 09:51:14,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10021,Martin Bortlik,CZ100212001,1,I-0,07 Apr 2026,Yes,16 Mar 2026,15 Mar 2026,16 Mar 2026,-,-,3,-,3,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,-,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,-,31 Mar 2026,-,30 Mar 2026,Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,28 Mar 2026,Day Not Applicable for Calculation,11,11,10,11,11,10,9,-,-,-,3,2,2,2,2,2,2,2,-,-,-,2,8,8,11,-,20 Apr 2026 09:27:58,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10021,Martin Bortlik,CZ100212001,1,I-2,20 Apr 2026,-,-,-,-,-,-,-,-,3,19 Apr 2026,-,18 Apr 2026,-,17 Apr 2026,-,16 Apr 2026,-,15 Apr 2026,-,14 Apr 2026,-,13 Apr 2026,-,12 Apr 2026,Day Not Applicable for Calculation,11 Apr 2026,Day Not Applicable for Calculation,10 Apr 2026,Day Not Applicable for Calculation,8,7,9,8,8,7,8,-,-,-,3,2,2,1,1,1,2,1,-,-,-,1,7,,,-,20 Apr 2026 09:29:01,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10021,Martin Bortlik,CZ100212001,1,I-4,05 May 2026,-,-,-,-,-,-,-,-,1,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,Day Not Applicable for Calculation,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,6,6,6,6,7,7,6,-,-,-,3,0,0,1,1,1,1,1,-,-,-,1,5,,,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222002,1,I-0,19 Feb 2026,Yes,11 Feb 2026,10 Feb 2026,11 Feb 2026,-,-,2,-,2,18 Feb 2026,-,17 Feb 2026,-,16 Feb 2026,-,15 Feb 2026,-,14 Feb 2026,-,13 Feb 2026,-,12 Feb 2026,-,11 Feb 2026,Endoscopy;Bowel Preparation for Procedure;Day Not Applicable for Calculation,10 Feb 2026,Bowel Preparation for Procedure;Day Not Applicable for Calculation,09 Feb 2026,Day Not Applicable for Calculation,3,2,2,3,4,3,2,-,-,-,1,1,1,0,0,0,2,2,-,-,-,1,4,4,6,-,19 Feb 2026 15:41:35,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222003,1,I-0,09 Mar 2026,Yes,11 Feb 2026,10 Feb 2026,11 Feb 2026,-,-,2,-,2,08 Mar 2026,-,07 Mar 2026,-,06 Mar 2026,-,05 Mar 2026,-,04 Mar 2026,-,03 Mar 2026,Missing Diary,02 Mar 2026,Missing Diary,01 Mar 2026,Missing Diary;Day Not Applicable for Calculation,28 Feb 2026,Missing Diary;Day Not Applicable for Calculation,27 Feb 2026,Missing Diary;Day Not Applicable for Calculation,7,7,6,6,7,-,-,-,-,-,3,2,2,2,2,2,-,-,-,-,-,2,7,7,9,-,22 Mar 2026 18:34:58,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222003,1,I-2,27 Mar 2026,-,-,-,-,-,-,-,-,2,26 Mar 2026,-,25 Mar 2026,-,24 Mar 2026,-,23 Mar 2026,-,22 Mar 2026,-,21 Mar 2026,-,20 Mar 2026,-,19 Mar 2026,Day Not Applicable for Calculation,18 Mar 2026,Day Not Applicable for Calculation,17 Mar 2026,Day Not Applicable for Calculation,7,3,3,3,5,5,5,-,-,-,2,0,0,1,1,1,1,2,-,-,-,1,5,,,-,27 Mar 2026 07:22:31,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222003,1,I-4,08 Apr 2026,-,-,-,-,-,-,-,-,2,07 Apr 2026,-,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,-,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,-,31 Mar 2026,Day Not Applicable for Calculation,30 Mar 2026,Day Not Applicable for Calculation,29 Mar 2026,Day Not Applicable for Calculation,3,3,4,4,5,4,3,-,-,-,2,1,0,0,2,1,1,2,-,-,-,1,5,,,-,08 Apr 2026 07:59:35,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222003,1,I-8,04 May 2026,-,-,-,-,-,-,-,-,2,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,-,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,24 Apr 2026,Missing Diary;Day Not Applicable for Calculation,3,5,3,3,3,2,3,-,-,-,1,0,0,0,0,0,0,0,-,-,-,0,3,,,-,04 May 2026 07:52:47,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222005,1,I-0,09 Apr 2026,Yes,08 Apr 2026,31 Mar 2026,01 Apr 2026,-,-,2,-,2,08 Apr 2026,Endoscopy,07 Apr 2026,-,06 Apr 2026,-,05 Apr 2026,-,04 Apr 2026,-,03 Apr 2026,-,02 Apr 2026,-,01 Apr 2026,Bowel Preparation for Procedure;Day Not Applicable for Calculation,31 Mar 2026,Bowel Preparation for Procedure;Day Not Applicable for Calculation,30 Mar 2026,-,-,3,3,4,3,4,3,-,-,3,1,-,2,2,2,2,2,2,-,-,2,2,5,5,7,-,-,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222005,1,I-2,22 Apr 2026,-,-,-,-,-,-,-,-,2,21 Apr 2026,-,20 Apr 2026,-,19 Apr 2026,-,18 Apr 2026,-,17 Apr 2026,-,16 Apr 2026,-,15 Apr 2026,-,14 Apr 2026,Day Not Applicable for Calculation,13 Apr 2026,Day Not Applicable for Calculation,12 Apr 2026,Day Not Applicable for Calculation,3,3,5,3,2,3,2,-,-,-,1,1,2,2,1,1,1,2,-,-,-,1,4,,,-,22 Apr 2026 14:03:33,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
77242113UCO3001,Adult,Czech Republic,DD5-CZ10022,Petr Hrabak,CZ100222005,1,I-4,05 May 2026,-,-,-,-,-,-,-,-,2,04 May 2026,-,03 May 2026,-,02 May 2026,-,01 May 2026,-,30 Apr 2026,-,29 Apr 2026,-,28 Apr 2026,-,27 Apr 2026,Day Not Applicable for Calculation,26 Apr 2026,Day Not Applicable for Calculation,25 Apr 2026,Day Not Applicable for Calculation,4,2,2,2,2,2,2,-,-,-,1,1,1,1,1,2,1,1,-,-,-,1,4,,,-,05 May 2026 07:30:02,N/A,N/A,N/A,N/A,N/A,N/A
|
||||||
|
@@ -0,0 +1,134 @@
|
|||||||
|
# Clario Report — 77242113UCO3001
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Verze | 1.2 |
|
||||||
|
| Datum | 2026-05-27 |
|
||||||
|
|
||||||
|
Sada skriptů pro import Clario CSV exportů (MayoScore, MayoDiary) do MongoDB a generování interaktivního Excel reportu pro studii 77242113UCO3001.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Přehled
|
||||||
|
|
||||||
|
Skript `create_report.py` generuje Excel report ze dvou MongoDB kolekcí (databáze `Clario`) do adresáře `U:\Dropbox\!!!Days\Downloads Z230\`.
|
||||||
|
|
||||||
|
Název výstupního souboru: `YYYY-MM-DD 77242113UCO3001 Clario Reports.xlsm`
|
||||||
|
|
||||||
|
Soubor je ve formátu `.xlsm` (Excel s makry). Při otevření je nutné **povolit makra**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spuštění
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python create_report.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Listy
|
||||||
|
|
||||||
|
### MayoScore
|
||||||
|
|
||||||
|
Jeden řádek = jeden záznam (pacient × visit).
|
||||||
|
Zdroj: `Clario.MayoScore` — 37 záznamů, 14 pacientů, 8 center.
|
||||||
|
Řazení: Site → Subject ID → Visit (I-0, I-2, I-4, I-8, I-12, Unscheduled).
|
||||||
|
|
||||||
|
Řádky s Visit = `I-0` a Modified Mayo Score < 5 jsou zobrazeny **červeně tučně**.
|
||||||
|
|
||||||
|
**Interaktivita:** Klik na libovolný řádek automaticky přepne na list EligibleDays a vyfiltruje záznamy pro daného pacienta a visit.
|
||||||
|
|
||||||
|
| Sloupec | Popis |
|
||||||
|
|---|---|
|
||||||
|
| Site | Kód centra |
|
||||||
|
| Subject ID | Číslo pacienta |
|
||||||
|
| Visit | Kód návštěvy |
|
||||||
|
| Visit Date | Datum návštěvy |
|
||||||
|
| Baseline Stool Frequency | Výchozí frekvence stolic |
|
||||||
|
| Central Endoscopy Score | Centrální endoskopické skóre |
|
||||||
|
| PGA Score | Celkové hodnocení lékaře |
|
||||||
|
| Stool Frequency Sub-score | Subscore frekvence stolic |
|
||||||
|
| Rectal Bleeding Sub-score | Subscore rektálního krvácení |
|
||||||
|
| Partial Mayo Score | Parciální Mayo skóre |
|
||||||
|
| Modified Mayo Score | Modifikované Mayo skóre |
|
||||||
|
| Full Mayo Score | Úplné Mayo skóre |
|
||||||
|
|
||||||
|
### MayoDiary
|
||||||
|
|
||||||
|
Jeden řádek = jeden denní záznam deníku pacienta.
|
||||||
|
Zdroj: `Clario.MayoDiary` — 1 098 záznamů, 20 pacientů, 10 center.
|
||||||
|
Řazení: Subject ID → Report Date.
|
||||||
|
|
||||||
|
| Sloupec | Popis |
|
||||||
|
|---|---|
|
||||||
|
| Subject ID | Číslo pacienta |
|
||||||
|
| Report Date | Datum záznamu |
|
||||||
|
| Baseline Stool Count | Výchozí počet stolic |
|
||||||
|
| Stool Frequency | Frekvence stolic daný den |
|
||||||
|
| MAYO050 | Popis rektálního krvácení |
|
||||||
|
| Not Applicable | Záznam nepřipadá v úvahu |
|
||||||
|
| Constipation | Zácpa |
|
||||||
|
| Diarrhea | Průjem |
|
||||||
|
| Irregularity | Nepravidelnost |
|
||||||
|
|
||||||
|
### EligibleDays
|
||||||
|
|
||||||
|
Jeden řádek = jeden eligible day (-1 až -10) z MayoScore, obohacený o data z MayoDiary pro stejného pacienta a datum.
|
||||||
|
Řazení: Subject ID → Visit → Den (-1 první).
|
||||||
|
|
||||||
|
Dny **nezahrnuté** do výpočtu skóre (Included = No): žluté pozadí, šedý font.
|
||||||
|
|
||||||
|
| Sloupec | Popis |
|
||||||
|
|---|---|
|
||||||
|
| Included | Byl den zahrnut do výpočtu Mayo skóre? (Yes/No) |
|
||||||
|
| Subject ID | Číslo pacienta |
|
||||||
|
| Visit | Kód návštěvy |
|
||||||
|
| Visit Date | Datum návštěvy |
|
||||||
|
| Day | Číslo dne (-1 až -10) |
|
||||||
|
| Report Date | Datum daného dne |
|
||||||
|
| Baseline Stool Count | Výchozí počet stolic |
|
||||||
|
| Stool Frequency | Frekvence stolic daný den (z MayoDiary) |
|
||||||
|
| MAYO050 | Popis rektálního krvácení (z MayoDiary) |
|
||||||
|
| Not Applicable | Záznam nepřipadá v úvahu (z MayoDiary) |
|
||||||
|
| Constipation | Zácpa (z MayoDiary) |
|
||||||
|
| Diarrhea | Průjem (z MayoDiary) |
|
||||||
|
| Irregularity | Nepravidelnost (z MayoDiary) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import dat do MongoDB
|
||||||
|
|
||||||
|
Skript `import_to_mongo.py` načte CSV soubory z adresáře `downloads/` a zapíše je do MongoDB.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python import_to_mongo.py # všechny CSV z downloads/
|
||||||
|
python import_to_mongo.py downloads/soubor.csv # jeden soubor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mapování souborů na kolekce
|
||||||
|
|
||||||
|
| Vzor v názvu souboru | Kolekce | Klíč záznamu |
|
||||||
|
|---|---|---|
|
||||||
|
| `MayoDiary` | `Clario.MayoDiary` | Subject ID + Form Number |
|
||||||
|
| `MayoScore` | `Clario.MayoScore` | Participant ID + Visit |
|
||||||
|
|
||||||
|
### Filtr
|
||||||
|
|
||||||
|
Importují se pouze řádky s `Country == "Czech Republic"`.
|
||||||
|
|
||||||
|
### Historie změn
|
||||||
|
|
||||||
|
Při změně datových polí se předchozí verze uloží do pole `history[]` dokumentu spolu s datem změny. Záznamy se nikdy nemažou.
|
||||||
|
|
||||||
|
Po zpracování se soubor přesune do `downloads/Zpracovano/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MongoDB
|
||||||
|
|
||||||
|
| Parametr | Hodnota |
|
||||||
|
|---|---|
|
||||||
|
| URI | `mongodb://192.168.1.76:27017` |
|
||||||
|
| Databáze | `Clario` |
|
||||||
|
| Kolekce | `Clario.MayoScore`, `Clario.MayoDiary` |
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
"""
|
||||||
|
create_report.py
|
||||||
|
Verze: 1.2
|
||||||
|
Datum: 2026-05-27
|
||||||
|
|
||||||
|
Generuje Excel report (.xlsm) pro studii 77242113UCO3001 z MongoDB databáze Clario.
|
||||||
|
Výstup: U:/Dropbox/!!!Days/Downloads Z230/YYYY-MM-DD 77242113UCO3001 Clario Reports.xlsm
|
||||||
|
|
||||||
|
Listy:
|
||||||
|
MayoScore — jeden řádek = pacient × visit; řádky I-0 s Modified Mayo < 5 červeně tučně
|
||||||
|
MayoDiary — jeden řádek = denní záznam deníku pacienta
|
||||||
|
EligibleDays — jeden řádek = jeden eligible day z MayoScore obohacený o data z MayoDiary;
|
||||||
|
included/excluded flag, excluded dny šedě na žlutém pozadí
|
||||||
|
|
||||||
|
VBA makro (Worksheet_SelectionChange na listu MayoScore):
|
||||||
|
Klik na řádek → automaticky přepne na EligibleDays a vyfiltruje záznamy
|
||||||
|
pro daného pacienta a visit. Vyžaduje povolení maker při otevření souboru.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
import xlwings as xw
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konfigurace
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MONGO_URI = "mongodb://192.168.1.76:27017"
|
||||||
|
DB_NAME = "Clario"
|
||||||
|
OUTPUT_DIR = Path(r"U:\Dropbox\!!!Days\Downloads Z230")
|
||||||
|
|
||||||
|
VISIT_ORDER = ["I-0", "I-2", "I-4", "I-8", "I-12"]
|
||||||
|
|
||||||
|
COLUMNS_SCORE = [
|
||||||
|
("Site", lambda d: d.get("site", {}).get("name", "")),
|
||||||
|
("Subject ID", lambda d: d.get("subject", {}).get("id", "")),
|
||||||
|
("Visit", lambda d: d["fields"].get("Visit", "")),
|
||||||
|
("Visit Date", lambda d: d["fields"].get("Visit Date", "")),
|
||||||
|
("Baseline Stool Frequency", lambda d: _num(d["fields"].get("Baseline Stool Frequency", ""))),
|
||||||
|
("Central Endoscopy Score", lambda d: _num(d["fields"].get("Central Endoscopy Score", ""))),
|
||||||
|
("PGA Score", lambda d: _num(d["fields"].get("PGA Score", ""))),
|
||||||
|
("Stool Frequency Sub-score", lambda d: _num(d["fields"].get("Stool Frequency Sub-score", ""))),
|
||||||
|
("Rectal Bleeding Sub-score", lambda d: _num(d["fields"].get("Rectal Bleeding Sub-score", ""))),
|
||||||
|
("Partial Mayo Score", lambda d: _num(d["fields"].get("Partial Mayo Score", ""))),
|
||||||
|
("Modified Mayo Score", lambda d: _num(d["fields"].get("Modified Mayo Score", ""))),
|
||||||
|
("Full Mayo Score", lambda d: _num(d["fields"].get("Full Mayo Score", ""))),
|
||||||
|
]
|
||||||
|
|
||||||
|
COLUMNS_DIARY = [
|
||||||
|
("Subject ID", lambda d: d.get("subject", {}).get("id", "")),
|
||||||
|
("Report Date", lambda d: d["fields"].get("Report Date", "")),
|
||||||
|
("Baseline Stool Count", lambda d: _num(d["fields"].get("Baseline Stool Count", ""))),
|
||||||
|
("Stool Frequency", lambda d: _num(d["fields"].get("Stool Frequency", ""))),
|
||||||
|
("MAYO050", lambda d: d["fields"].get("MAYO050", "")),
|
||||||
|
("Not Applicable", lambda d: d["fields"].get("Not Applicable", "")),
|
||||||
|
("Constipation", lambda d: d["fields"].get("Constipation", "")),
|
||||||
|
("Diarrhea", lambda d: d["fields"].get("Diarrhea", "")),
|
||||||
|
("Irregularity", lambda d: d["fields"].get("Irregularity", "")),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _num(value):
|
||||||
|
"""Převede číselný string na int, jinak vrátí původní hodnotu nebo None."""
|
||||||
|
if value == "" or value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _visit_sort_key(doc):
|
||||||
|
visit = doc["fields"].get("Visit", "")
|
||||||
|
try:
|
||||||
|
idx = VISIT_ORDER.index(visit)
|
||||||
|
except ValueError:
|
||||||
|
idx = len(VISIT_ORDER)
|
||||||
|
return (doc.get("site", {}).get("name", ""), doc.get("subject", {}).get("id", ""), idx, visit)
|
||||||
|
|
||||||
|
|
||||||
|
def _iso_to_date(value):
|
||||||
|
"""ISO string → Python date pro Excel."""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(value).date()
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Styly
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
HEADER_FILL = PatternFill("solid", fgColor="1F497D")
|
||||||
|
HEADER_FONT = Font(bold=True, color="FFFFFF", size=10)
|
||||||
|
CELL_FONT = Font(size=10)
|
||||||
|
ALIGN_CTR = Alignment(horizontal="center", vertical="center", wrap_text=False)
|
||||||
|
ALIGN_LEFT = Alignment(horizontal="left", vertical="center")
|
||||||
|
|
||||||
|
THIN = Side(style="thin", color="BFBFBF")
|
||||||
|
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||||||
|
|
||||||
|
# zebra
|
||||||
|
FILL_ODD = PatternFill("solid", fgColor="FFFFFF")
|
||||||
|
FILL_EVEN = PatternFill("solid", fgColor="EBF1DE")
|
||||||
|
|
||||||
|
SCORE_COLS = {"Partial Mayo Score", "Modified Mayo Score", "Full Mayo Score"}
|
||||||
|
SCORE_FILL = PatternFill("solid", fgColor="FFC7CE") # červená pro skóre ≥ 5 (placeholder — nepoužíváme podmíněné formátování)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sestavení sheetu
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_sheet(ws, docs, columns, date_cols, center_cols, col_widths, row_font_fn=None):
|
||||||
|
headers = [c[0] for c in columns]
|
||||||
|
|
||||||
|
for col_idx, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||||
|
cell.font = HEADER_FONT
|
||||||
|
cell.fill = HEADER_FILL
|
||||||
|
cell.alignment = ALIGN_CTR
|
||||||
|
cell.border = BORDER
|
||||||
|
ws.row_dimensions[1].height = 28
|
||||||
|
|
||||||
|
for row_idx, doc in enumerate(docs, 2):
|
||||||
|
fill = FILL_EVEN if row_idx % 2 == 0 else FILL_ODD
|
||||||
|
font = row_font_fn(doc) if row_font_fn else CELL_FONT
|
||||||
|
for col_idx, (col_name, getter) in enumerate(columns, 1):
|
||||||
|
value = getter(doc)
|
||||||
|
if col_name in date_cols and isinstance(value, str):
|
||||||
|
value = _iso_to_date(value)
|
||||||
|
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
|
cell.font = font
|
||||||
|
cell.fill = fill
|
||||||
|
cell.border = BORDER
|
||||||
|
cell.alignment = ALIGN_CTR if col_name in center_cols else ALIGN_LEFT
|
||||||
|
|
||||||
|
for col_idx, (col_name, _) in enumerate(columns, 1):
|
||||||
|
ws.column_dimensions[get_column_letter(col_idx)].width = col_widths.get(col_name, 14)
|
||||||
|
|
||||||
|
for col_name in date_cols:
|
||||||
|
if col_name in headers:
|
||||||
|
letter = get_column_letter(headers.index(col_name) + 1)
|
||||||
|
for row_idx in range(2, len(docs) + 2):
|
||||||
|
ws[f"{letter}{row_idx}"].number_format = "DD-MMM-YYYY"
|
||||||
|
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1"
|
||||||
|
|
||||||
|
|
||||||
|
def _score_row_font(doc):
|
||||||
|
visit = doc["fields"].get("Visit", "")
|
||||||
|
try:
|
||||||
|
mod_mayo = int(doc["fields"].get("Modified Mayo Score", ""))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
mod_mayo = None
|
||||||
|
if visit == "I-0" and mod_mayo is not None and mod_mayo < 5:
|
||||||
|
return Font(size=10, bold=True, color="FF0000")
|
||||||
|
return CELL_FONT
|
||||||
|
|
||||||
|
|
||||||
|
def build_mayo_score_sheet(ws, docs):
|
||||||
|
_build_sheet(
|
||||||
|
ws, docs, COLUMNS_SCORE,
|
||||||
|
date_cols={"Visit Date"},
|
||||||
|
center_cols={"Visit", "Central Endoscopy Score", "PGA Score",
|
||||||
|
"Stool Frequency Sub-score", "Rectal Bleeding Sub-score",
|
||||||
|
"Partial Mayo Score", "Modified Mayo Score", "Full Mayo Score",
|
||||||
|
"Baseline Stool Frequency"},
|
||||||
|
col_widths={
|
||||||
|
"Site": 18, "Subject ID": 16, "Visit": 12, "Visit Date": 14,
|
||||||
|
"Baseline Stool Frequency": 14, "Central Endoscopy Score": 14,
|
||||||
|
"PGA Score": 10, "Stool Frequency Sub-score": 14,
|
||||||
|
"Rectal Bleeding Sub-score": 14, "Partial Mayo Score": 14,
|
||||||
|
"Modified Mayo Score": 14, "Full Mayo Score": 13,
|
||||||
|
},
|
||||||
|
row_font_fn=_score_row_font,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_mayo_diary_sheet(ws, docs):
|
||||||
|
_build_sheet(
|
||||||
|
ws, docs, COLUMNS_DIARY,
|
||||||
|
date_cols={"Report Date"},
|
||||||
|
center_cols={"Baseline Stool Count", "Stool Frequency", "Not Applicable",
|
||||||
|
"Constipation", "Diarrhea", "Irregularity"},
|
||||||
|
col_widths={
|
||||||
|
"Subject ID": 16, "Report Date": 14, "Baseline Stool Count": 14,
|
||||||
|
"Stool Frequency": 14, "MAYO050": 48, "Not Applicable": 14,
|
||||||
|
"Constipation": 14, "Diarrhea": 12, "Irregularity": 14,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_eligible_days_sheet(ws, score_docs, diary_docs):
|
||||||
|
# Lookup diary records by (subject_id, date_part YYYY-MM-DD)
|
||||||
|
diary_lookup: dict[tuple, dict] = {}
|
||||||
|
for d in diary_docs:
|
||||||
|
subj = d.get("subject", {}).get("id", "")
|
||||||
|
date_iso = d["fields"].get("Report Date", "")
|
||||||
|
date_part = date_iso[:10] if date_iso else ""
|
||||||
|
if subj and date_part:
|
||||||
|
diary_lookup[(subj, date_part)] = d
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
"Included", "Subject ID", "Visit", "Visit Date", "Day",
|
||||||
|
"Report Date", "Baseline Stool Count", "Stool Frequency",
|
||||||
|
"MAYO050", "Not Applicable", "Constipation", "Diarrhea", "Irregularity",
|
||||||
|
]
|
||||||
|
col_widths = {
|
||||||
|
"Included": 10, "Subject ID": 16, "Visit": 10, "Visit Date": 14, "Day": 8,
|
||||||
|
"Report Date": 14, "Baseline Stool Count": 14, "Stool Frequency": 14,
|
||||||
|
"MAYO050": 48, "Not Applicable": 14, "Constipation": 14,
|
||||||
|
"Diarrhea": 12, "Irregularity": 14,
|
||||||
|
}
|
||||||
|
center_cols = {"Included", "Visit", "Day", "Baseline Stool Count", "Stool Frequency",
|
||||||
|
"Not Applicable", "Constipation", "Diarrhea", "Irregularity"}
|
||||||
|
date_cols = {"Visit Date", "Report Date"}
|
||||||
|
no_fill = PatternFill("solid", fgColor="FFF2CC") # žlutá pro excluded dny
|
||||||
|
|
||||||
|
for col_idx, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||||
|
cell.font = HEADER_FONT
|
||||||
|
cell.fill = HEADER_FILL
|
||||||
|
cell.alignment = ALIGN_CTR
|
||||||
|
cell.border = BORDER
|
||||||
|
ws.row_dimensions[1].height = 28
|
||||||
|
|
||||||
|
row_idx = 2
|
||||||
|
for score_doc in score_docs:
|
||||||
|
subj = score_doc.get("subject", {}).get("id", "")
|
||||||
|
visit = score_doc["fields"].get("Visit", "")
|
||||||
|
visit_date = score_doc["fields"].get("Visit Date", "")
|
||||||
|
|
||||||
|
for n in range(1, 11):
|
||||||
|
day_date_iso = score_doc["fields"].get(f"Eligible Day (-{n})")
|
||||||
|
if not day_date_iso or day_date_iso == "-":
|
||||||
|
continue
|
||||||
|
date_part = day_date_iso[:10]
|
||||||
|
excl_reason = score_doc["fields"].get(f"Day (-{n}) Excluded Reason(s)", "")
|
||||||
|
included = "No" if excl_reason and excl_reason != "-" else "Yes"
|
||||||
|
|
||||||
|
diary = diary_lookup.get((subj, date_part), {})
|
||||||
|
df = diary.get("fields", {})
|
||||||
|
|
||||||
|
fill = no_fill if included == "No" else (FILL_EVEN if row_idx % 2 == 0 else FILL_ODD)
|
||||||
|
font = Font(size=10, color="808080") if included == "No" else CELL_FONT
|
||||||
|
|
||||||
|
values = [
|
||||||
|
included,
|
||||||
|
subj,
|
||||||
|
visit,
|
||||||
|
_iso_to_date(visit_date) if isinstance(visit_date, str) else visit_date,
|
||||||
|
f"-{n}",
|
||||||
|
_iso_to_date(day_date_iso),
|
||||||
|
_num(df.get("Baseline Stool Count", "")),
|
||||||
|
_num(df.get("Stool Frequency", "")),
|
||||||
|
df.get("MAYO050", ""),
|
||||||
|
df.get("Not Applicable", ""),
|
||||||
|
df.get("Constipation", ""),
|
||||||
|
df.get("Diarrhea", ""),
|
||||||
|
df.get("Irregularity", ""),
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_idx, (header, value) in enumerate(zip(headers, values), 1):
|
||||||
|
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
|
cell.font = font
|
||||||
|
cell.fill = fill
|
||||||
|
cell.border = BORDER
|
||||||
|
if header in date_cols:
|
||||||
|
cell.number_format = "DD-MMM-YYYY"
|
||||||
|
cell.alignment = ALIGN_CTR if header in center_cols else ALIGN_LEFT
|
||||||
|
|
||||||
|
row_idx += 1
|
||||||
|
|
||||||
|
for col_idx, header in enumerate(headers, 1):
|
||||||
|
ws.column_dimensions[get_column_letter(col_idx)].width = col_widths.get(header, 14)
|
||||||
|
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000)
|
||||||
|
client.admin.command("ping")
|
||||||
|
db = client[DB_NAME]
|
||||||
|
|
||||||
|
score_docs = list(db["Clario.MayoScore"].find({}))
|
||||||
|
diary_docs = list(db["Clario.MayoDiary"].find({}))
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
score_docs.sort(key=_visit_sort_key)
|
||||||
|
diary_docs.sort(key=lambda d: (
|
||||||
|
d.get("subject", {}).get("id", ""),
|
||||||
|
d["fields"].get("Report Date", ""),
|
||||||
|
))
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws_score = wb.active
|
||||||
|
ws_score.title = "MayoScore"
|
||||||
|
build_mayo_score_sheet(ws_score, score_docs)
|
||||||
|
|
||||||
|
ws_diary = wb.create_sheet("MayoDiary")
|
||||||
|
build_mayo_diary_sheet(ws_diary, diary_docs)
|
||||||
|
|
||||||
|
ws_days = wb.create_sheet("EligibleDays")
|
||||||
|
build_eligible_days_sheet(ws_days, score_docs, diary_docs)
|
||||||
|
|
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
filename = f"{today} 77242113UCO3001 Clario Reports.xlsx"
|
||||||
|
output_path = OUTPUT_DIR / filename
|
||||||
|
|
||||||
|
# Uložit jako .xlsx nejdřív, pak přepsat na .xlsm přes xlwings + injektovat VBA
|
||||||
|
xlsx_path = output_path.with_suffix(".xlsx")
|
||||||
|
xlsm_path = output_path.with_suffix(".xlsm")
|
||||||
|
wb.save(str(xlsx_path))
|
||||||
|
|
||||||
|
inject_vba(xlsx_path, xlsm_path)
|
||||||
|
xlsx_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
print(f"Uloženo: {xlsm_path}")
|
||||||
|
print(f"MayoScore: {len(score_docs)} záznamů")
|
||||||
|
print(f"MayoDiary: {len(diary_docs)} záznamů")
|
||||||
|
print(f"EligibleDays: generováno z {len(score_docs)} score záznamů")
|
||||||
|
|
||||||
|
|
||||||
|
def inject_vba(xlsx_path: Path, xlsm_path: Path) -> None:
|
||||||
|
vba_code = '''\
|
||||||
|
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
|
||||||
|
If Target.Row < 2 Or Target.Column < 1 Then Exit Sub
|
||||||
|
If Target.Rows.Count > 1 Then Exit Sub
|
||||||
|
|
||||||
|
Dim subjectId As String
|
||||||
|
Dim visit As String
|
||||||
|
subjectId = CStr(Me.Cells(Target.Row, 2).Value)
|
||||||
|
visit = CStr(Me.Cells(Target.Row, 3).Value)
|
||||||
|
|
||||||
|
If subjectId = "" Or visit = "" Then Exit Sub
|
||||||
|
|
||||||
|
Dim ws As Worksheet
|
||||||
|
On Error Resume Next
|
||||||
|
Set ws = ThisWorkbook.Sheets("EligibleDays")
|
||||||
|
On Error GoTo 0
|
||||||
|
If ws Is Nothing Then Exit Sub
|
||||||
|
|
||||||
|
Application.ScreenUpdating = False
|
||||||
|
|
||||||
|
ws.AutoFilterMode = False
|
||||||
|
ws.Range("A1").AutoFilter
|
||||||
|
ws.Range("A1").AutoFilter Field:=2, Criteria1:=subjectId
|
||||||
|
ws.Range("A1").AutoFilter Field:=3, Criteria1:=visit
|
||||||
|
|
||||||
|
ws.Activate
|
||||||
|
ws.Range("A2").Select
|
||||||
|
|
||||||
|
Application.ScreenUpdating = True
|
||||||
|
End Sub
|
||||||
|
'''
|
||||||
|
|
||||||
|
app = xw.App(visible=False)
|
||||||
|
try:
|
||||||
|
wb = app.books.open(str(xlsx_path))
|
||||||
|
# Najdi VBComponent odpovídající listu "MayoScore" podle tab názvu
|
||||||
|
vb_comp = None
|
||||||
|
for comp in wb.api.VBProject.VBComponents:
|
||||||
|
if comp.Type == 100: # xlSheet
|
||||||
|
try:
|
||||||
|
if comp.Properties("Name").Value == "MayoScore":
|
||||||
|
vb_comp = comp
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if vb_comp is None:
|
||||||
|
# fallback: první sheet (Sheet1)
|
||||||
|
vb_comp = wb.api.VBProject.VBComponents("Sheet1")
|
||||||
|
vb_comp.CodeModule.AddFromString(vba_code)
|
||||||
|
wb.api.SaveAs(str(xlsm_path), FileFormat=52) # 52 = xlOpenXMLWorkbookMacroEnabled
|
||||||
|
wb.close()
|
||||||
|
finally:
|
||||||
|
app.quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
"""
|
||||||
|
import_to_mongo.py
|
||||||
|
Verze: 1.0
|
||||||
|
Datum: 2026-05-27
|
||||||
|
|
||||||
|
Import Clario CSV do MongoDB (databáze: Clario).
|
||||||
|
|
||||||
|
Kolekce: Clario.MayoDiary / Clario.MayoScore (dle názvu souboru)
|
||||||
|
Filtr: pouze řádky s Country == "Czech Republic"
|
||||||
|
Klíč: MayoDiary → Subject ID + Form Number
|
||||||
|
MayoScore → Participant ID + Visit
|
||||||
|
Historie: při změně fields se stará verze uloží do pole history[]
|
||||||
|
Po importu přesune zpracované CSV do downloads/Zpracovano/
|
||||||
|
|
||||||
|
Použití:
|
||||||
|
python import_to_mongo.py # importuje všechny CSV z downloads/
|
||||||
|
python import_to_mongo.py downloads/konkretni.csv # jeden soubor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pymongo import MongoClient, ASCENDING
|
||||||
|
|
||||||
|
MONGO_URI = "mongodb://192.168.1.76:27017"
|
||||||
|
DB_NAME = "Clario"
|
||||||
|
DOWNLOADS_DIR = Path(__file__).parent / "downloads"
|
||||||
|
PROCESSED_DIR = DOWNLOADS_DIR / "Zpracovano"
|
||||||
|
|
||||||
|
COUNTRY_FILTER = "Czech Republic"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konfigurace kolekcí
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
COLLECTION_CONFIG = {
|
||||||
|
"MayoDiary": {
|
||||||
|
"collection": "Clario.MayoDiary",
|
||||||
|
"subject_col": "Subject ID",
|
||||||
|
"key_cols": ("Subject ID", "Form Number"),
|
||||||
|
},
|
||||||
|
"MayoScore": {
|
||||||
|
"collection": "Clario.MayoScore",
|
||||||
|
"subject_col": "Participant ID",
|
||||||
|
"key_cols": ("Participant ID", "Visit"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DATE_FORMATS = [
|
||||||
|
"%d-%b-%Y ",
|
||||||
|
"%d-%b-%Y",
|
||||||
|
"%d-%b-%Y %H:%M:%S",
|
||||||
|
"%d %b %Y %H:%M:%S",
|
||||||
|
"%d %b %Y %H:%M:%S:%f",
|
||||||
|
"%d %b %Y",
|
||||||
|
"%d %B %Y",
|
||||||
|
"%Y%m%d %H:%M:%S.%f",
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
"%m/%d/%Y %I:%M:%S %p",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def clean_colname(name: str) -> str:
|
||||||
|
"""Odstraní BOM a okolní uvozovky/mezery z názvu sloupce."""
|
||||||
|
return name.lstrip("").strip().strip('"')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(value: str) -> str | None:
|
||||||
|
v = value.strip()
|
||||||
|
for fmt in DATE_FORMATS:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(v, fmt.strip())
|
||||||
|
return dt.replace(tzinfo=timezone.utc).isoformat()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_snapshot_date(filename: str) -> str:
|
||||||
|
match = re.match(r"(\d{4}-\d{2}-\d{2})", Path(filename).name)
|
||||||
|
return match.group(1) if match else datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def detect_collection_type(filename: str) -> str | None:
|
||||||
|
"""Vrátí klíč do COLLECTION_CONFIG nebo None."""
|
||||||
|
stem = Path(filename).stem
|
||||||
|
for key in COLLECTION_CONFIG:
|
||||||
|
if key in stem:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CSV → dokument
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def map_row(row: dict, col_type: str) -> dict:
|
||||||
|
cfg = COLLECTION_CONFIG[col_type]
|
||||||
|
doc: dict = {}
|
||||||
|
fields: dict = {}
|
||||||
|
|
||||||
|
cleaned = {clean_colname(k): v.strip() if v else "" for k, v in row.items()}
|
||||||
|
|
||||||
|
subject_col = cfg["subject_col"]
|
||||||
|
doc["subject"] = {"id": cleaned.get(subject_col, "")}
|
||||||
|
doc["site"] = {"name": cleaned.get("Site", "")}
|
||||||
|
doc["country"] = cleaned.get("Country", "")
|
||||||
|
doc["study"] = cleaned.get("Protocol", "")
|
||||||
|
|
||||||
|
key_parts = [cleaned.get(c, "") for c in cfg["key_cols"]]
|
||||||
|
doc["recordKey"] = "_".join(key_parts)
|
||||||
|
|
||||||
|
skip_top = {"Protocol", "Country", "Site", subject_col}
|
||||||
|
for col, value in cleaned.items():
|
||||||
|
if col in skip_top:
|
||||||
|
continue
|
||||||
|
if not value or value == "-":
|
||||||
|
continue
|
||||||
|
parsed = parse_date(value)
|
||||||
|
fields[col] = parsed if parsed else value
|
||||||
|
|
||||||
|
doc["fields"] = fields
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import jednoho souboru
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def import_file(csv_path: str, db) -> dict:
|
||||||
|
filename = Path(csv_path).name
|
||||||
|
col_type = detect_collection_type(filename)
|
||||||
|
if col_type is None:
|
||||||
|
print(f" Preskakuji (neznamy typ): {filename}")
|
||||||
|
return {"skipped": True}
|
||||||
|
|
||||||
|
cfg = COLLECTION_CONFIG[col_type]
|
||||||
|
col_name = cfg["collection"]
|
||||||
|
snapshot_date = extract_snapshot_date(filename)
|
||||||
|
collection = db[col_name]
|
||||||
|
|
||||||
|
inserted = changed = unchanged = filtered_out = 0
|
||||||
|
|
||||||
|
with open(csv_path, encoding="utf-8-sig", newline="") as f:
|
||||||
|
reader = csv.DictReader(f, delimiter=",", quotechar='"')
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
cleaned_row = {clean_colname(k): v for k, v in row.items()}
|
||||||
|
country = cleaned_row.get("Country", "").strip()
|
||||||
|
if country != COUNTRY_FILTER:
|
||||||
|
filtered_out += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
doc = map_row(row, col_type)
|
||||||
|
record_key = doc.get("recordKey")
|
||||||
|
if not record_key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
doc["sourceFile"] = filename
|
||||||
|
|
||||||
|
existing = collection.find_one({"recordKey": record_key})
|
||||||
|
|
||||||
|
if existing is None:
|
||||||
|
doc["firstSeen"] = snapshot_date
|
||||||
|
doc["lastSeen"] = snapshot_date
|
||||||
|
doc["history"] = []
|
||||||
|
collection.insert_one(doc)
|
||||||
|
inserted += 1
|
||||||
|
|
||||||
|
elif existing.get("fields") != doc["fields"]:
|
||||||
|
old_entry = {
|
||||||
|
"date": existing.get("lastSeen", snapshot_date),
|
||||||
|
"fields": existing["fields"],
|
||||||
|
}
|
||||||
|
update_doc = {k: v for k, v in doc.items()}
|
||||||
|
update_doc["lastSeen"] = snapshot_date
|
||||||
|
collection.update_one(
|
||||||
|
{"_id": existing["_id"]},
|
||||||
|
{
|
||||||
|
"$push": {"history": old_entry},
|
||||||
|
"$set": update_doc,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
changed += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
collection.update_one(
|
||||||
|
{"_id": existing["_id"]},
|
||||||
|
{"$set": {"lastSeen": snapshot_date, "sourceFile": filename}},
|
||||||
|
)
|
||||||
|
unchanged += 1
|
||||||
|
|
||||||
|
collection.create_index([("recordKey", ASCENDING)], unique=True)
|
||||||
|
collection.create_index([("subject.id", ASCENDING)])
|
||||||
|
collection.create_index([("site.name", ASCENDING)])
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"collection": col_name,
|
||||||
|
"snapshot": snapshot_date,
|
||||||
|
"inserted": inserted,
|
||||||
|
"changed": changed,
|
||||||
|
"unchanged": unchanged,
|
||||||
|
"filtered_out": filtered_out,
|
||||||
|
}
|
||||||
|
print(f" {col_name} [{snapshot_date}]: +{inserted} new, ~{changed} changed, ={unchanged} same, -{filtered_out} non-CZ")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
paths: list[Path] = []
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
p = Path(arg)
|
||||||
|
if p.is_file():
|
||||||
|
paths.append(p)
|
||||||
|
else:
|
||||||
|
print(f"Soubor nenalezen: {arg}")
|
||||||
|
else:
|
||||||
|
paths = sorted(DOWNLOADS_DIR.glob("*.csv"))
|
||||||
|
|
||||||
|
if not paths:
|
||||||
|
print("Zadne CSV soubory k importu.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Nalezeno {len(paths)} souboru.\n")
|
||||||
|
|
||||||
|
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000)
|
||||||
|
client.admin.command("ping")
|
||||||
|
db = client[DB_NAME]
|
||||||
|
|
||||||
|
PROCESSED_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
total = {"inserted": 0, "changed": 0, "unchanged": 0}
|
||||||
|
|
||||||
|
for csv_path in paths:
|
||||||
|
print(f"Import: {csv_path.name}")
|
||||||
|
stats = import_file(str(csv_path), db)
|
||||||
|
if not stats.get("skipped"):
|
||||||
|
for k in total:
|
||||||
|
total[k] += stats.get(k, 0)
|
||||||
|
|
||||||
|
dest = PROCESSED_DIR / csv_path.name
|
||||||
|
shutil.move(str(csv_path), str(dest))
|
||||||
|
print(f" -> presunut do Zpracovano/")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
print(f"\nCelkem: +{total['inserted']} new, ~{total['changed']} changed, ={total['unchanged']} same")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
"""
|
||||||
|
AE Import — scrapes Adverse Events from EvaMed DRY study and upserts into MongoDB.
|
||||||
|
|
||||||
|
Run repeatedly; only stores field-level changes (delta) in history[].
|
||||||
|
Unique key: patient_code + event_number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = f"{BASE_URL}?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
# Direct filtered URL: CZ1 (center_id=2), Adverse Event (formtype=120), all records
|
||||||
|
LIST_URL = f"{BASE_URL}?module=monitoring&class=formslisting¢er_id=2&formtype=120&l=ALL"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
|
||||||
|
MONGO_HOST = "192.168.1.76"
|
||||||
|
DB_NAME = "Dry"
|
||||||
|
COLLECTION = "AE"
|
||||||
|
SESSION_FILE = Path(__file__).parent / "session.json"
|
||||||
|
|
||||||
|
DATE_RE = re.compile(r"^(\d{2})/(\d{2})/(\d{4})$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(value):
|
||||||
|
"""Parse DD/MM/YYYY → datetime, digit-only → int, else str. None if empty."""
|
||||||
|
if not value or not value.strip():
|
||||||
|
return None
|
||||||
|
v = value.strip()
|
||||||
|
m = DATE_RE.fullmatch(v)
|
||||||
|
if m:
|
||||||
|
return datetime(int(m.group(3)), int(m.group(2)), int(m.group(1)))
|
||||||
|
if re.fullmatch(r"\d+", v):
|
||||||
|
return int(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
async def do_login(page):
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator("#login").fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('input[value="Connection"]')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_form_ids(page):
|
||||||
|
"""Return list of {form_id, patient_code} from the filtered forms list."""
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
return await page.evaluate("""() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('a[title="Open form"]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const m = href.match(/id=(\\d+)/);
|
||||||
|
if (!m) return;
|
||||||
|
// Patient code: "Open directory" link in the same row, text of the anchor
|
||||||
|
const row = a.closest('tr');
|
||||||
|
const dirLink = row ? row.querySelector('a[title="Open directory"]') : null;
|
||||||
|
const patientCode = dirLink ? dirLink.innerText.trim() : '';
|
||||||
|
results.push({ formId: m[1], patientCode: patientCode });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_form_fields(page, form_id):
|
||||||
|
"""Navigate to AE form and extract all field values."""
|
||||||
|
url = f"{BASE_URL}?module=dossier&class=file&event=show&id={form_id}#fiche"
|
||||||
|
await page.goto(url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
raw = await page.evaluate("""() => {
|
||||||
|
const fields = {};
|
||||||
|
document.querySelectorAll('.tableauFormulaire span.label').forEach(label => {
|
||||||
|
const key = label.innerText.trim();
|
||||||
|
const valEl = label.nextElementSibling;
|
||||||
|
fields[key] = valEl ? valEl.innerText.trim() || null : null;
|
||||||
|
});
|
||||||
|
return fields;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
# Parse values into correct Python types
|
||||||
|
parsed = {}
|
||||||
|
for k, v in raw.items():
|
||||||
|
if k == '_patient_code':
|
||||||
|
parsed[k] = v
|
||||||
|
else:
|
||||||
|
parsed[k] = parse_value(v)
|
||||||
|
|
||||||
|
parsed['_form_id'] = int(form_id)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_ae(collection, doc, now):
|
||||||
|
patient_code = doc.get('_patient_code') or ''
|
||||||
|
event_number = doc.get('Event Number')
|
||||||
|
|
||||||
|
key = {"patient_code": patient_code, "event_number": event_number}
|
||||||
|
existing = collection.find_one(key)
|
||||||
|
|
||||||
|
# Fields we track changes for (exclude internal fields)
|
||||||
|
skip = {'_patient_code', '_form_id'}
|
||||||
|
data = {k: v for k, v in doc.items() if k not in skip}
|
||||||
|
|
||||||
|
if existing is None:
|
||||||
|
collection.insert_one({
|
||||||
|
**key,
|
||||||
|
"_form_id": doc['_form_id'],
|
||||||
|
"data": data,
|
||||||
|
"history": [],
|
||||||
|
"first_seen_at": now,
|
||||||
|
"last_seen_at": now,
|
||||||
|
"deleted_at": None,
|
||||||
|
})
|
||||||
|
print(f" NEW {patient_code} AE#{event_number}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delta: compare data with stored data
|
||||||
|
old_data = existing.get("data", {})
|
||||||
|
changes = {}
|
||||||
|
for k in set(data) | set(old_data):
|
||||||
|
old_v = old_data.get(k)
|
||||||
|
new_v = data.get(k)
|
||||||
|
if old_v != new_v:
|
||||||
|
changes[k] = {"old": old_v, "new": new_v}
|
||||||
|
|
||||||
|
update = {"$set": {"last_seen_at": now, "deleted_at": None}}
|
||||||
|
if changes:
|
||||||
|
update["$set"]["data"] = data
|
||||||
|
update["$push"] = {"history": {"timestamp": now, "changes": changes}}
|
||||||
|
print(f" CHANGED {patient_code} AE#{event_number} -> {list(changes.keys())}")
|
||||||
|
else:
|
||||||
|
print(f" ok {patient_code} AE#{event_number}")
|
||||||
|
|
||||||
|
collection.update_one(key, update)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
mongo = MongoClient(MONGO_HOST)
|
||||||
|
col = mongo[DB_NAME][COLLECTION]
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
# Reuse saved session if available
|
||||||
|
if SESSION_FILE.exists():
|
||||||
|
context = await browser.new_context(storage_state=str(SESSION_FILE))
|
||||||
|
print("Loaded saved session")
|
||||||
|
else:
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# Check if we need to log in
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
if "authentification" in page.url:
|
||||||
|
print("Logging in...")
|
||||||
|
await do_login(page)
|
||||||
|
await context.storage_state(path=str(SESSION_FILE))
|
||||||
|
print("Session saved")
|
||||||
|
else:
|
||||||
|
print("Session valid")
|
||||||
|
|
||||||
|
# Get all AE form IDs from filtered list
|
||||||
|
form_infos = await get_form_ids(page)
|
||||||
|
current_ids = {info['formId'] for info in form_infos}
|
||||||
|
print(f"Found {len(form_infos)} AE forms")
|
||||||
|
|
||||||
|
# Scrape and upsert each form
|
||||||
|
for info in form_infos:
|
||||||
|
fid = info['formId']
|
||||||
|
print(f"Scraping form {fid} ({info['patientCode']})...")
|
||||||
|
doc = await extract_form_fields(page, fid)
|
||||||
|
# Patient code comes from the list (more reliable than form page heading)
|
||||||
|
doc['_patient_code'] = info['patientCode']
|
||||||
|
upsert_ae(col, doc, now)
|
||||||
|
|
||||||
|
# Mark as deleted any forms that disappeared from the list
|
||||||
|
for rec in col.find({"deleted_at": None}, {"_form_id": 1, "patient_code": 1, "event_number": 1}):
|
||||||
|
if str(rec.get('_form_id', '')) not in current_ids:
|
||||||
|
col.update_one({"_id": rec["_id"]}, {"$set": {"deleted_at": now}})
|
||||||
|
print(f" DELETED form_id={rec['_form_id']} ({rec.get('patient_code')} AE#{rec.get('event_number')})")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
mongo.close()
|
||||||
|
print(f"\nDone — {len(form_infos)} forms processed at {now.isoformat()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""
|
||||||
|
Device Deficiency Import — scrapes DD forms from EvaMed DRY study and upserts into MongoDB.
|
||||||
|
|
||||||
|
Run repeatedly; only stores field-level changes (delta) in history[].
|
||||||
|
Unique key: _form_id (each DD form has a unique ID in EvaMed).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = f"{BASE_URL}?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
LIST_URL = f"{BASE_URL}?module=monitoring&class=formslisting¢er_id=2&formtype=121&l=ALL"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
|
||||||
|
MONGO_HOST = "192.168.1.76"
|
||||||
|
DB_NAME = "Dry"
|
||||||
|
COLLECTION = "DevDeficiency"
|
||||||
|
SESSION_FILE = Path(__file__).parent / "session.json"
|
||||||
|
|
||||||
|
DATE_RE = re.compile(r"^(\d{2})/(\d{2})/(\d{4})$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(value):
|
||||||
|
"""Parse DD/MM/YYYY -> datetime, digit-only -> int, else str. None if empty."""
|
||||||
|
if not value or not value.strip():
|
||||||
|
return None
|
||||||
|
v = value.strip()
|
||||||
|
m = DATE_RE.fullmatch(v)
|
||||||
|
if m:
|
||||||
|
return datetime(int(m.group(3)), int(m.group(2)), int(m.group(1)))
|
||||||
|
if re.fullmatch(r"\d+", v):
|
||||||
|
return int(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
async def do_login(page):
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator("#login").fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('input[value="Connection"]')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_form_ids(page):
|
||||||
|
"""Return list of {formId, patientCode} from the filtered forms list."""
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
return await page.evaluate("""() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('a[title="Open form"]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const m = href.match(/id=(\\d+)/);
|
||||||
|
if (!m) return;
|
||||||
|
const row = a.closest('tr');
|
||||||
|
const dirLink = row ? row.querySelector('a[title="Open directory"]') : null;
|
||||||
|
const patientCode = dirLink ? dirLink.innerText.trim() : '';
|
||||||
|
results.push({ formId: m[1], patientCode: patientCode });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_form_fields(page, form_id):
|
||||||
|
"""Navigate to DD form and extract all field values."""
|
||||||
|
url = f"{BASE_URL}?module=dossier&class=file&event=show&id={form_id}#fiche"
|
||||||
|
await page.goto(url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
raw = await page.evaluate("""() => {
|
||||||
|
const fields = {};
|
||||||
|
document.querySelectorAll('.tableauFormulaire span.label').forEach(label => {
|
||||||
|
const key = label.innerText.trim();
|
||||||
|
const valEl = label.nextElementSibling;
|
||||||
|
fields[key] = valEl ? valEl.innerText.trim() || null : null;
|
||||||
|
});
|
||||||
|
return fields;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
for k, v in raw.items():
|
||||||
|
parsed[k] = parse_value(v)
|
||||||
|
|
||||||
|
parsed['_form_id'] = int(form_id)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_dd(collection, doc, patient_code, now):
|
||||||
|
form_id = doc['_form_id']
|
||||||
|
key = {"_form_id": form_id}
|
||||||
|
existing = collection.find_one(key)
|
||||||
|
|
||||||
|
skip = {'_form_id'}
|
||||||
|
data = {k: v for k, v in doc.items() if k not in skip}
|
||||||
|
|
||||||
|
if existing is None:
|
||||||
|
collection.insert_one({
|
||||||
|
**key,
|
||||||
|
"patient_code": patient_code,
|
||||||
|
"data": data,
|
||||||
|
"history": [],
|
||||||
|
"first_seen_at": now,
|
||||||
|
"last_seen_at": now,
|
||||||
|
"deleted_at": None,
|
||||||
|
})
|
||||||
|
print(f" NEW {patient_code} DD form_id={form_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
old_data = existing.get("data", {})
|
||||||
|
changes = {}
|
||||||
|
for k in set(data) | set(old_data):
|
||||||
|
old_v = old_data.get(k)
|
||||||
|
new_v = data.get(k)
|
||||||
|
if old_v != new_v:
|
||||||
|
changes[k] = {"old": old_v, "new": new_v}
|
||||||
|
|
||||||
|
update = {"$set": {"last_seen_at": now, "deleted_at": None, "patient_code": patient_code}}
|
||||||
|
if changes:
|
||||||
|
update["$set"]["data"] = data
|
||||||
|
update["$push"] = {"history": {"timestamp": now, "changes": changes}}
|
||||||
|
print(f" CHANGED {patient_code} DD form_id={form_id} -> {list(changes.keys())}")
|
||||||
|
else:
|
||||||
|
print(f" ok {patient_code} DD form_id={form_id}")
|
||||||
|
|
||||||
|
collection.update_one(key, update)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
mongo = MongoClient(MONGO_HOST)
|
||||||
|
col = mongo[DB_NAME][COLLECTION]
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
if SESSION_FILE.exists():
|
||||||
|
context = await browser.new_context(storage_state=str(SESSION_FILE))
|
||||||
|
print("Loaded saved session")
|
||||||
|
else:
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
if "authentification" in page.url:
|
||||||
|
print("Logging in...")
|
||||||
|
await do_login(page)
|
||||||
|
await context.storage_state(path=str(SESSION_FILE))
|
||||||
|
print("Session saved")
|
||||||
|
else:
|
||||||
|
print("Session valid")
|
||||||
|
|
||||||
|
form_infos = await get_form_ids(page)
|
||||||
|
current_ids = {info['formId'] for info in form_infos}
|
||||||
|
print(f"Found {len(form_infos)} DD forms")
|
||||||
|
|
||||||
|
for info in form_infos:
|
||||||
|
fid = info['formId']
|
||||||
|
print(f"Scraping form {fid} ({info['patientCode']})...")
|
||||||
|
doc = await extract_form_fields(page, fid)
|
||||||
|
upsert_dd(col, doc, info['patientCode'], now)
|
||||||
|
|
||||||
|
for rec in col.find({"deleted_at": None}, {"_form_id": 1, "patient_code": 1}):
|
||||||
|
if str(rec.get('_form_id', '')) not in current_ids:
|
||||||
|
col.update_one({"_id": rec["_id"]}, {"$set": {"deleted_at": now}})
|
||||||
|
print(f" DELETED form_id={rec['_form_id']} ({rec.get('patient_code')})")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
mongo.close()
|
||||||
|
print(f"\nDone -- {len(form_infos)} forms processed at {now.isoformat()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
"""Explorační skript — přihlášení do EvaMed CRF, načtení všech formulářů, nalezení AE."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = "https://prod.evamed.com/etude/soft/index.php?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
SCREENSHOTS_DIR = "screenshots"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator('#login').fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('text=Connection')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
print("Logged in")
|
||||||
|
|
||||||
|
# 2. Go to Forms list
|
||||||
|
await page.goto(f"{BASE_URL}?module=monitoring&class=formslisting")
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
print("Forms list loaded (page 1)")
|
||||||
|
|
||||||
|
# 3. Switch to ALL records
|
||||||
|
await page.select_option('select[name="l"]', 'ALL')
|
||||||
|
print("Switched to ALL, waiting for all rows to load...")
|
||||||
|
|
||||||
|
await page.wait_for_function(
|
||||||
|
"() => document.querySelectorAll('tr').length > 3200",
|
||||||
|
timeout=120000
|
||||||
|
)
|
||||||
|
print(f"All rows loaded")
|
||||||
|
|
||||||
|
# 4. Count total rows and find AE rows
|
||||||
|
stats = await page.evaluate("""() => {
|
||||||
|
const rows = document.querySelectorAll('tr');
|
||||||
|
let totalRows = rows.length;
|
||||||
|
let aeRows = [];
|
||||||
|
let formCodes = {};
|
||||||
|
|
||||||
|
rows.forEach((row, idx) => {
|
||||||
|
const cells = Array.from(row.querySelectorAll('td'));
|
||||||
|
cells.forEach(cell => {
|
||||||
|
// collect all unique form codes from the Formcode column
|
||||||
|
});
|
||||||
|
// Look for AE in any cell
|
||||||
|
const cellTexts = cells.map(c => c.innerText.trim());
|
||||||
|
// Formcode is typically column index 8 based on the header
|
||||||
|
if (cells.length > 8) {
|
||||||
|
const formcode = cells[8]?.innerText?.trim();
|
||||||
|
formCodes[formcode] = (formCodes[formcode] || 0) + 1;
|
||||||
|
if (formcode === 'AE') {
|
||||||
|
aeRows.push({
|
||||||
|
rowIndex: idx,
|
||||||
|
subject: cells[0]?.innerText?.trim(),
|
||||||
|
formcode: formcode,
|
||||||
|
formName: cells[9]?.innerText?.trim(),
|
||||||
|
allCells: cellTexts,
|
||||||
|
links: Array.from(row.querySelectorAll('a')).map(a => ({
|
||||||
|
href: a.getAttribute('href'),
|
||||||
|
title: a.title || '',
|
||||||
|
text: a.innerText.trim().substring(0, 50)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { totalRows, aeCount: aeRows.length, aeRows: aeRows.slice(0, 5), formCodes };
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"Total rows: {stats['totalRows']}")
|
||||||
|
print(f"AE rows found: {stats['aeCount']}")
|
||||||
|
print(f"Form codes: {stats['formCodes']}")
|
||||||
|
|
||||||
|
if stats['aeRows']:
|
||||||
|
print(f"\nFirst AE row sample:")
|
||||||
|
ae = stats['aeRows'][0]
|
||||||
|
print(f" Subject: {ae['subject']}")
|
||||||
|
print(f" All cells: {ae['allCells']}")
|
||||||
|
print(f" Links: {ae['links']}")
|
||||||
|
|
||||||
|
# 5. Open first AE form
|
||||||
|
for link in ae['links']:
|
||||||
|
if link.get('href') and 'id=' in link['href']:
|
||||||
|
ae_url = link['href']
|
||||||
|
if not ae_url.startswith('http'):
|
||||||
|
ae_url = f"https://prod.evamed.com/etude/soft/{ae_url}"
|
||||||
|
print(f"\nOpening AE form: {ae_url}")
|
||||||
|
await page.goto(ae_url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.screenshot(path=f"{SCREENSHOTS_DIR}/05_ae_form.png", full_page=True)
|
||||||
|
print("Screenshot: AE form")
|
||||||
|
|
||||||
|
# Extract all fields from the form
|
||||||
|
fields = await page.evaluate("""() => {
|
||||||
|
// Try input-group pattern
|
||||||
|
const groups = document.querySelectorAll('.input-group');
|
||||||
|
let inputGroupFields = Array.from(groups).map(g => ({
|
||||||
|
html: g.outerHTML.substring(0, 800),
|
||||||
|
text: g.innerText.trim().substring(0, 300)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Try label/value pattern in tableauFormulaire
|
||||||
|
const tableFields = [];
|
||||||
|
document.querySelectorAll('.tableauFormulaire td, .tableauFormulaire th').forEach(el => {
|
||||||
|
tableFields.push({
|
||||||
|
tag: el.tagName,
|
||||||
|
className: el.className,
|
||||||
|
text: el.innerText.trim().substring(0, 200)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try all form inputs
|
||||||
|
const inputs = [];
|
||||||
|
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||||
|
inputs.push({
|
||||||
|
type: el.type,
|
||||||
|
name: el.name,
|
||||||
|
id: el.id,
|
||||||
|
value: el.value?.substring(0, 200),
|
||||||
|
className: el.className
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { inputGroupFields, tableFields: tableFields.slice(0, 50), inputs: inputs.slice(0, 50) };
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\nInput groups: {len(fields['inputGroupFields'])}")
|
||||||
|
for i, f in enumerate(fields['inputGroupFields'][:10]):
|
||||||
|
print(f" [{i}] {f['text'][:120]}")
|
||||||
|
|
||||||
|
print(f"\nTable fields: {len(fields['tableFields'])}")
|
||||||
|
for i, f in enumerate(fields['tableFields'][:20]):
|
||||||
|
print(f" [{i}] <{f['tag']} class='{f['className']}'> {f['text'][:100]}")
|
||||||
|
|
||||||
|
print(f"\nForm inputs: {len(fields['inputs'])}")
|
||||||
|
for i, f in enumerate(fields['inputs'][:20]):
|
||||||
|
print(f" [{i}] {f['type']} name={f['name']} id={f['id']} val={f['value'][:60] if f['value'] else ''}")
|
||||||
|
|
||||||
|
# Save full form HTML for analysis
|
||||||
|
form_html = await page.content()
|
||||||
|
with open(f"{SCREENSHOTS_DIR}/05_ae_form_full.html", "w", encoding="utf-8") as f:
|
||||||
|
f.write(form_html)
|
||||||
|
print("Saved: full AE form HTML")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("NO AE ROWS FOUND!")
|
||||||
|
await page.screenshot(path=f"{SCREENSHOTS_DIR}/04_all_forms.png", full_page=False)
|
||||||
|
# Dump all unique form codes for debugging
|
||||||
|
print("Available form codes:", list(stats['formCodes'].keys())[:30])
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
print("Done")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import os
|
||||||
|
os.makedirs(SCREENSHOTS_DIR, exist_ok=True)
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""Exploration script — Device Deficiency forms in EvaMed DRY study."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = f"{BASE_URL}?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
LIST_URL = f"{BASE_URL}?module=monitoring&class=formslisting¢er_id=2&formtype=121&l=ALL"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR = Path(__file__).parent / "screenshots_dd"
|
||||||
|
SESSION_FILE = Path(__file__).parent / "session.json"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
if SESSION_FILE.exists():
|
||||||
|
context = await browser.new_context(storage_state=str(SESSION_FILE))
|
||||||
|
print("Loaded saved session")
|
||||||
|
else:
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# Login if needed
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
if "authentification" in page.url:
|
||||||
|
print("Logging in...")
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator("#login").fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('input[value="Connection"]')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await context.storage_state(path=str(SESSION_FILE))
|
||||||
|
print("Session saved")
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
else:
|
||||||
|
print("Session valid")
|
||||||
|
|
||||||
|
await page.screenshot(path=str(SCREENSHOTS_DIR / "01_dd_listing.png"), full_page=False)
|
||||||
|
print("Screenshot: DD listing")
|
||||||
|
|
||||||
|
# Get all DD form links from the listing
|
||||||
|
form_infos = await page.evaluate("""() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('a[title="Open form"]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const m = href.match(/id=(\\d+)/);
|
||||||
|
if (!m) return;
|
||||||
|
const row = a.closest('tr');
|
||||||
|
const dirLink = row ? row.querySelector('a[title="Open directory"]') : null;
|
||||||
|
const patientCode = dirLink ? dirLink.innerText.trim() : '';
|
||||||
|
const cells = row ? Array.from(row.querySelectorAll('td')).map(c => c.innerText.trim()) : [];
|
||||||
|
results.push({ formId: m[1], patientCode, cells });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\nFound {len(form_infos)} Device Deficiency forms")
|
||||||
|
for i, info in enumerate(form_infos[:5]):
|
||||||
|
print(f" [{i}] form_id={info['formId']} patient={info['patientCode']} cells={info['cells']}")
|
||||||
|
|
||||||
|
if not form_infos:
|
||||||
|
print("NO DD FORMS FOUND!")
|
||||||
|
# Save HTML for debugging
|
||||||
|
html = await page.content()
|
||||||
|
(SCREENSHOTS_DIR / "01_dd_listing.html").write_text(html, encoding="utf-8")
|
||||||
|
await browser.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Open the first DD form
|
||||||
|
first = form_infos[0]
|
||||||
|
form_url = f"{BASE_URL}?module=dossier&class=file&event=show&id={first['formId']}#fiche"
|
||||||
|
print(f"\nOpening DD form: {form_url}")
|
||||||
|
await page.goto(form_url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.screenshot(path=str(SCREENSHOTS_DIR / "02_dd_form.png"), full_page=True)
|
||||||
|
print("Screenshot: DD form")
|
||||||
|
|
||||||
|
# Extract fields using the same pattern as AE (span.label + span.valeur)
|
||||||
|
fields_label_value = await page.evaluate("""() => {
|
||||||
|
const fields = [];
|
||||||
|
document.querySelectorAll('.tableauFormulaire span.label').forEach(label => {
|
||||||
|
const key = label.innerText.trim();
|
||||||
|
const valEl = label.nextElementSibling;
|
||||||
|
const value = valEl ? valEl.innerText.trim() : null;
|
||||||
|
const valClass = valEl ? valEl.className : '';
|
||||||
|
fields.push({ key, value, valueClass: valClass });
|
||||||
|
});
|
||||||
|
return fields;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\n=== Fields (span.label -> span.valeur) : {len(fields_label_value)} ===")
|
||||||
|
for f in fields_label_value:
|
||||||
|
print(f" {f['key']:40s} = {f['value']}")
|
||||||
|
|
||||||
|
# Also explore table structure for any additional patterns
|
||||||
|
table_structure = await page.evaluate("""() => {
|
||||||
|
const sections = [];
|
||||||
|
document.querySelectorAll('.tableauFormulaire').forEach((table, ti) => {
|
||||||
|
const rows = [];
|
||||||
|
table.querySelectorAll('tr').forEach((tr, ri) => {
|
||||||
|
const cells = Array.from(tr.querySelectorAll('td, th')).map(c => ({
|
||||||
|
tag: c.tagName,
|
||||||
|
class: c.className,
|
||||||
|
colspan: c.colSpan,
|
||||||
|
text: c.innerText.trim().substring(0, 200),
|
||||||
|
childSpans: Array.from(c.querySelectorAll('span')).map(s => ({
|
||||||
|
class: s.className,
|
||||||
|
text: s.innerText.trim().substring(0, 200)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
if (cells.length > 0) rows.push({ rowIndex: ri, cells });
|
||||||
|
});
|
||||||
|
sections.push({ tableIndex: ti, rowCount: rows.length, rows: rows.slice(0, 30) });
|
||||||
|
});
|
||||||
|
return sections;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\n=== Table structure: {len(table_structure)} tableauFormulaire blocks ===")
|
||||||
|
for sec in table_structure:
|
||||||
|
print(f"\n Table #{sec['tableIndex']} ({sec['rowCount']} rows):")
|
||||||
|
for row in sec['rows'][:15]:
|
||||||
|
for cell in row['cells']:
|
||||||
|
spans_info = " | ".join(f"[{s['class']}]{s['text'][:60]}" for s in cell['childSpans'])
|
||||||
|
print(f" row{row['rowIndex']} <{cell['tag']} class='{cell['class']}' colspan={cell['colspan']}> "
|
||||||
|
f"{cell['text'][:80]}")
|
||||||
|
if spans_info:
|
||||||
|
print(f" spans: {spans_info}")
|
||||||
|
|
||||||
|
# Save full form HTML
|
||||||
|
html = await page.content()
|
||||||
|
(SCREENSHOTS_DIR / "02_dd_form.html").write_text(html, encoding="utf-8")
|
||||||
|
print("\nSaved: full DD form HTML")
|
||||||
|
|
||||||
|
# Save extracted data as JSON for easy review
|
||||||
|
result = {
|
||||||
|
"form_id": first['formId'],
|
||||||
|
"patient_code": first['patientCode'],
|
||||||
|
"listing_cells": first['cells'],
|
||||||
|
"fields": fields_label_value,
|
||||||
|
"table_structure": table_structure,
|
||||||
|
}
|
||||||
|
(SCREENSHOTS_DIR / "dd_form_data.json").write_text(
|
||||||
|
json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print("Saved: dd_form_data.json")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
print("\nDone")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""Exploration script — V0 Implantation Visit forms (formtype=10) in EvaMed DRY study."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = f"{BASE_URL}?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
LIST_URL = f"{BASE_URL}?module=monitoring&class=formslisting¢er_id=2&formtype=10&l=ALL"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR = Path(__file__).parent / "screenshots_surgery"
|
||||||
|
SESSION_FILE = Path(__file__).parent / "session.json"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
if SESSION_FILE.exists():
|
||||||
|
context = await browser.new_context(storage_state=str(SESSION_FILE))
|
||||||
|
print("Loaded saved session")
|
||||||
|
else:
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# Login if needed
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
if "authentification" in page.url:
|
||||||
|
print("Logging in...")
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator("#login").fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('input[value="Connection"]')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await context.storage_state(path=str(SESSION_FILE))
|
||||||
|
print("Session saved")
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
else:
|
||||||
|
print("Session valid")
|
||||||
|
|
||||||
|
await page.screenshot(path=str(SCREENSHOTS_DIR / "01_listing.png"), full_page=False)
|
||||||
|
print("Screenshot: listing")
|
||||||
|
|
||||||
|
# Get all form links from the listing
|
||||||
|
form_infos = await page.evaluate("""() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('a[title="Open form"]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const m = href.match(/id=(\\d+)/);
|
||||||
|
if (!m) return;
|
||||||
|
const row = a.closest('tr');
|
||||||
|
const dirLink = row ? row.querySelector('a[title="Open directory"]') : null;
|
||||||
|
const patientCode = dirLink ? dirLink.innerText.trim() : '';
|
||||||
|
const cells = row ? Array.from(row.querySelectorAll('td')).map(c => c.innerText.trim()) : [];
|
||||||
|
results.push({ formId: m[1], patientCode, cells });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\nFound {len(form_infos)} Implantation Visit forms")
|
||||||
|
for i, info in enumerate(form_infos[:5]):
|
||||||
|
print(f" [{i}] form_id={info['formId']} patient={info['patientCode']} cells={info['cells']}")
|
||||||
|
|
||||||
|
if not form_infos:
|
||||||
|
print("NO FORMS FOUND!")
|
||||||
|
html = await page.content()
|
||||||
|
(SCREENSHOTS_DIR / "01_listing.html").write_text(html, encoding="utf-8")
|
||||||
|
await browser.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Open the first form
|
||||||
|
first = form_infos[0]
|
||||||
|
form_url = f"{BASE_URL}?module=dossier&class=file&event=show&id={first['formId']}#fiche"
|
||||||
|
print(f"\nOpening form: {form_url}")
|
||||||
|
await page.goto(form_url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.screenshot(path=str(SCREENSHOTS_DIR / "02_form.png"), full_page=True)
|
||||||
|
print("Screenshot: form")
|
||||||
|
|
||||||
|
# Extract fields using span.label + span.valeur pattern
|
||||||
|
fields_label_value = await page.evaluate("""() => {
|
||||||
|
const fields = [];
|
||||||
|
document.querySelectorAll('.tableauFormulaire span.label').forEach(label => {
|
||||||
|
const key = label.innerText.trim();
|
||||||
|
const valEl = label.nextElementSibling;
|
||||||
|
const value = valEl ? valEl.innerText.trim() : null;
|
||||||
|
const valClass = valEl ? valEl.className : '';
|
||||||
|
fields.push({ key, value, valueClass: valClass });
|
||||||
|
});
|
||||||
|
return fields;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\n=== Fields (span.label -> span.valeur) : {len(fields_label_value)} ===")
|
||||||
|
for f in fields_label_value:
|
||||||
|
key = f['key'].encode('ascii', 'replace').decode()
|
||||||
|
val = (f['value'] or '').encode('ascii', 'replace').decode()
|
||||||
|
print(f" {key:50s} = {val}")
|
||||||
|
|
||||||
|
# Save full form HTML
|
||||||
|
html = await page.content()
|
||||||
|
(SCREENSHOTS_DIR / "02_form.html").write_text(html, encoding="utf-8")
|
||||||
|
print("\nSaved: full form HTML")
|
||||||
|
|
||||||
|
# Save extracted data as JSON
|
||||||
|
result = {
|
||||||
|
"form_id": first['formId'],
|
||||||
|
"patient_code": first['patientCode'],
|
||||||
|
"listing_cells": first['cells'],
|
||||||
|
"fields": fields_label_value,
|
||||||
|
}
|
||||||
|
(SCREENSHOTS_DIR / "form_data.json").write_text(
|
||||||
|
json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print("Saved: form_data.json")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
print("\nDone")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
EvaMed DRY study — form type selectors.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Filter URL: formtype={value}
|
||||||
|
Checkbox: #formtype_{value}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
await page.check('#formtype_120') # Adverse Event
|
||||||
|
await page.check('#formtype_121') # Device Deficiency
|
||||||
|
"""
|
||||||
|
|
||||||
|
FORMTYPES = {
|
||||||
|
1: "Eligibility Criteria",
|
||||||
|
8: "V-3 : Baseline Within 3 weeks before the implant procedure",
|
||||||
|
122: "V-3 : Day 1 - Voiding diary",
|
||||||
|
123: "V-3 : Day 2 - Voiding diary",
|
||||||
|
124: "V-3 : Day 3 - Voiding diary",
|
||||||
|
2: "V-3 : ICIQ-MLUTS",
|
||||||
|
3: "V-3 : ICIQ-LUTSQol",
|
||||||
|
4: "V-3 : EQ-5D-5L",
|
||||||
|
5: "V-3 : MSHQ",
|
||||||
|
9: "V-1 : Phone Call (1 Week before Implantation)",
|
||||||
|
10: "V0 : Implantation Visit",
|
||||||
|
11: "DV : Discharge Visit",
|
||||||
|
12: "V1 : 6 Weeks Post-Operative Period (Device Activation)",
|
||||||
|
129: "V1 : Device Activation",
|
||||||
|
125: "V1 : Physician Usability Questionnaire",
|
||||||
|
13: "V2 : 8 Weeks Post-Operative Period",
|
||||||
|
130: "V2 : Device Adjustment",
|
||||||
|
127: "V2 : Physician Usability Questionnaire",
|
||||||
|
14: "V3 : 3 Months Post-Operative Period",
|
||||||
|
131: "V3 : Device Adjustment",
|
||||||
|
128: "V3 : Physician Usability Questionnaire",
|
||||||
|
15: "V4 - Phone Call (10 Weeks after device activation)",
|
||||||
|
16: "V5 : 3 Months Post-Device Activation",
|
||||||
|
138: "V5 : Day 1 - Voiding diary",
|
||||||
|
139: "V5 : Day 2 - Voiding diary",
|
||||||
|
140: "V5 : Day 3 - Voiding diary",
|
||||||
|
132: "V5 : Device Adjustment",
|
||||||
|
135: "V5 : Unlocking additional modes",
|
||||||
|
17: "V5 : ICIQ-MLUTS",
|
||||||
|
18: "V5 : ICIQ-LUTSQol",
|
||||||
|
19: "V5 : EQ-5D-5L",
|
||||||
|
20: "V5 : MSHQ",
|
||||||
|
21: "V5 : PGI-I",
|
||||||
|
22: "V5 : Subject Usability Questionnaire",
|
||||||
|
23: "V5 : Physician Usability Questionnaire",
|
||||||
|
24: "V6 : Phone Call (22 Weeks after Device activation)",
|
||||||
|
25: "V7 : 6 Months Post-Device Activation",
|
||||||
|
142: "V7 : Day 1 - Voiding diary",
|
||||||
|
143: "V7 : Day 2 - Voiding diary",
|
||||||
|
144: "V7 : Day 3 - Voiding diary",
|
||||||
|
150: "V7 : Device Adjustment",
|
||||||
|
180: "V7 - Unlocking additional mode",
|
||||||
|
26: "V7 : ICIQ-MLUTS",
|
||||||
|
27: "V7 : ICIQ-LUTSQol",
|
||||||
|
29: "V7 : EQ-5D-5L",
|
||||||
|
31: "V7 : MSHQ",
|
||||||
|
32: "V7 : PGI-I",
|
||||||
|
33: "V7 : Subject Usability Questionnaire",
|
||||||
|
34: "V7 : Physician Usability Questionnaire",
|
||||||
|
35: "V8 : Phone Call (46 Weeks after Device activation)",
|
||||||
|
36: "V9 : 12 Months Post-Device Activation",
|
||||||
|
146: "V9 : Day 1 - Voiding diary",
|
||||||
|
147: "V9 : Day 2 - Voiding diary",
|
||||||
|
148: "V9 : Day 3 - Voiding diary",
|
||||||
|
151: "V9 : Device Adjustment",
|
||||||
|
181: "V9 - Unlocking additional mode",
|
||||||
|
37: "V9 : ICIQ-MLUTS",
|
||||||
|
38: "V9 : ICIQ-LUTSQol",
|
||||||
|
39: "V9 : EQ-5D-5L",
|
||||||
|
40: "V9 : MSHQ",
|
||||||
|
41: "V9 : PGI-I",
|
||||||
|
42: "V9 : Subject Usability Questionnaire",
|
||||||
|
43: "V9 : Physician Usability Questionnaire",
|
||||||
|
44: "V10 : Long-term annual Follow-up",
|
||||||
|
153: "V10 : Device Adjustment",
|
||||||
|
162: "V10 : Unlocking additional modes",
|
||||||
|
45: "V10 : ICIQ-MLUTS",
|
||||||
|
47: "V10 : ICIQ-LUTSQol",
|
||||||
|
48: "V10 : EQ-5D-5L",
|
||||||
|
49: "V10 : MSHQ",
|
||||||
|
50: "V10 : PGI-I",
|
||||||
|
51: "V10 : Subject Usability Questionnaire",
|
||||||
|
52: "V10 : Physician Usability Questionnaire",
|
||||||
|
53: "V11 : Long-term annual Follow-up",
|
||||||
|
154: "V11 : Device Adjustment",
|
||||||
|
163: "V11 : Unlocking additional modes",
|
||||||
|
54: "V11 : ICIQ-MLUTS",
|
||||||
|
55: "V11 : ICIQ-LUTSQol",
|
||||||
|
56: "V11 : EQ-5D-5L",
|
||||||
|
57: "V11 : MSHQ",
|
||||||
|
58: "V11 : PGI-I",
|
||||||
|
59: "V11 : Subject Usability Questionnaire",
|
||||||
|
60: "V11 : Physician Usability Questionnaire",
|
||||||
|
119: "UV : Unscheduled Visit",
|
||||||
|
183: "UV : Voiding diary - Day 1 to Day 3 (if applicable)",
|
||||||
|
173: "UV : Device Adjustment",
|
||||||
|
174: "UV : Unlocking additional modes",
|
||||||
|
175: "UV : Physician Usability Questionnaire",
|
||||||
|
177: "Concomitant Medication",
|
||||||
|
120: "Adverse Event",
|
||||||
|
121: "Device Deficiency",
|
||||||
|
178: "Deviation",
|
||||||
|
182: "Subsequent surgery",
|
||||||
|
176: "Study Termination",
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
<table style="margin: auto;">
|
||||||
|
<tbody><tr>
|
||||||
|
<td>
|
||||||
|
<a href="?module=monitoring&class=formslisting&center_code=&patientfile_code=&center_id=&filetype=&formtype=&dateinc_inf=&dateinc_sup=&dateint_inf=&dateint_sup=&datet0_inf=&datet0_sup=&datefiche_inf=&datefiche_sup=&delai_inf=&delai_sup=&visits=&status=&pff_exists=&nb_open_query_fields=&tx_remplissage_inf=&tx_remplissage_sup=&l=0&DOWNLOAD=Excel2007">
|
||||||
|
<img src="img/dl/ms-excel2007-128x128.png" alt="Download"><br>
|
||||||
|
MS Excel 2007 (.xlsx)</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="?module=monitoring&class=formslisting&center_code=&patientfile_code=&center_id=&filetype=&formtype=&dateinc_inf=&dateinc_sup=&dateint_inf=&dateint_sup=&datet0_inf=&datet0_sup=&datefiche_inf=&datefiche_sup=&delai_inf=&delai_sup=&visits=&status=&pff_exists=&nb_open_query_fields=&tx_remplissage_inf=&tx_remplissage_sup=&l=0&DOWNLOAD=Excel5">
|
||||||
|
<img src="img/dl/ms-excel5-128x128.png" alt="Download"><br>
|
||||||
|
MS Excel 5 (.xls)</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="?module=monitoring&class=formslisting&center_code=&patientfile_code=&center_id=&filetype=&formtype=&dateinc_inf=&dateinc_sup=&dateint_inf=&dateint_sup=&datet0_inf=&datet0_sup=&datefiche_inf=&datefiche_sup=&delai_inf=&delai_sup=&visits=&status=&pff_exists=&nb_open_query_fields=&tx_remplissage_inf=&tx_remplissage_sup=&l=0&DOWNLOAD=csv">
|
||||||
|
<img src="img/dl/csv-128x128.png" alt="Download"><br>
|
||||||
|
CSV (.csv)</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="?module=monitoring&class=formslisting&center_code=&patientfile_code=&center_id=&filetype=&formtype=&dateinc_inf=&dateinc_sup=&dateint_inf=&dateint_sup=&datet0_inf=&datet0_sup=&datefiche_inf=&datefiche_sup=&delai_inf=&delai_sup=&visits=&status=&pff_exists=&nb_open_query_fields=&tx_remplissage_inf=&tx_remplissage_sup=&l=0&DOWNLOAD=XML">
|
||||||
|
<img src="img/dl/xml-128x128.png" alt="Download"><br>
|
||||||
|
XML (.xml)</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 161 KiB |
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"form_id": "461",
|
||||||
|
"patient_code": "CZ1-01",
|
||||||
|
"listing_cells": [
|
||||||
|
"CZ1-01",
|
||||||
|
"Male",
|
||||||
|
"Subject",
|
||||||
|
"16/02/2024",
|
||||||
|
"28/02/2024",
|
||||||
|
"17/04/2024",
|
||||||
|
"CZ1 - Fakultní Thomayerova nemocnice",
|
||||||
|
"DD",
|
||||||
|
"Device Deficiency",
|
||||||
|
"0",
|
||||||
|
"0",
|
||||||
|
"30",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"23/02/2025",
|
||||||
|
"Accepted",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"100",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"1",
|
||||||
|
"Open",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": "Date of Onset",
|
||||||
|
"value": "23/02/2025",
|
||||||
|
"valueClass": "valeur DTEONSET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Date device deficiency discovered by site",
|
||||||
|
"value": "04/03/2025",
|
||||||
|
"valueClass": "valeur DTEDEVIC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Title of device deficiency",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1 - Type of Device Deficiency",
|
||||||
|
"value": "Malfunction (failure of device to operate as intended when used per IFU and protocol)",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Specify other type of device deficiency",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2 - Description of Event",
|
||||||
|
"value": "Patient reported to site device malfunction. Device switched to emergency regime and kept being permanently open. It is not possible to control device. The sponsor informed abou the deficiency immediately. Patient has no pain, no urine retention, as per X-ray, positioning of control unit and cuff is correct.",
|
||||||
|
"valueClass": "valeur DESC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3 - Information about Device(s) (name, lot and serial number)",
|
||||||
|
"value": "Remote control 052300185, control unit 210011, cuff 22-0224",
|
||||||
|
"valueClass": "valeur INFO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4 - Consequence of Device Deficiency",
|
||||||
|
"value": "Led to adverse event",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Lead to AE number",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Action(s) taken",
|
||||||
|
"value": "Other",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "If other action(s), specify",
|
||||||
|
"value": "Implementation of the new sofware leading to continence, time to activation emergency regime increased from 8 to 12hours",
|
||||||
|
"valueClass": "valeur ACTIONP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Event Outcome",
|
||||||
|
"value": "Resolved without Sequelae",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Event End Date",
|
||||||
|
"value": "07/04/2025",
|
||||||
|
"valueClass": "valeur DTEEND"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"table_structure": [
|
||||||
|
{
|
||||||
|
"tableIndex": 0,
|
||||||
|
"rowCount": 0,
|
||||||
|
"rows": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 208 KiB |
@@ -0,0 +1,385 @@
|
|||||||
|
{
|
||||||
|
"form_id": "10",
|
||||||
|
"patient_code": "CZ1-01",
|
||||||
|
"listing_cells": [
|
||||||
|
"CZ1-01",
|
||||||
|
"Male",
|
||||||
|
"Subject",
|
||||||
|
"16/02/2024",
|
||||||
|
"28/02/2024",
|
||||||
|
"17/04/2024",
|
||||||
|
"CZ1 - Fakultní Thomayerova nemocnice",
|
||||||
|
"VISIT0",
|
||||||
|
"V0 : Implantation Visit",
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"1",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"28/02/2024",
|
||||||
|
"Accepted",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"1",
|
||||||
|
"Open",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": "Date of surgery (or date of the attempt)",
|
||||||
|
"value": "28/02/2024",
|
||||||
|
"valueClass": "valeur DTET0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Has the patient been implanted with ARTUS®?",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "How many implantation attempts have there been?",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Please explain why and describe the difficulty(ies) experienced:",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "The results of the 24-hour Pad Weight Test have been entered in the Baseline visit to validate the inclusion of the subject",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Was the clinical examination performed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Exam date",
|
||||||
|
"value": "27/02/2024",
|
||||||
|
"valueClass": "valeur DTEEXAM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Weight",
|
||||||
|
"value": "114 kg",
|
||||||
|
"valueClass": "valeur WGT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Body Mass Index",
|
||||||
|
"value": "32.3 kg/cm²",
|
||||||
|
"valueClass": "valeur BMI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Explain why clinical examination was not performed",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "The charge of the TWO Remote Controls was done correctly before the surgery (for 5 hours each)",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All steps for the \"Start procedure\" of the two Remote Controls have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All steps for \"Pairing procedure\" of the Remote Control and the Control Unit have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All the steps of the 'Calibration procedure' have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "LOT of the ARTUS® Cuff S",
|
||||||
|
"value": "22-0224",
|
||||||
|
"valueClass": "valeur CUFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SN of the ARTUS® Control Unit",
|
||||||
|
"value": "210011",
|
||||||
|
"valueClass": "valeur CONTSN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SN of the ARTUS® Remote Control #1",
|
||||||
|
"value": "052300185",
|
||||||
|
"valueClass": "valeur REMOSN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SN of the ARTUS® Remote Control #2",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "The Back-up Material was used",
|
||||||
|
"value": "No",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "LOT of the ARTUS® Cuff S",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SN of the ARTUS® Control Unit",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SN of the ARTUS® Remote Control",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Beginning of the surgery, start of the incision (hh:mm)",
|
||||||
|
"value": "10:00",
|
||||||
|
"valueClass": "valeur START"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "End of the surgery, closure of the incision (hh:mm)",
|
||||||
|
"value": "11:15",
|
||||||
|
"valueClass": "valeur STOP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "What type of anesthesia performed ?",
|
||||||
|
"value": "General",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Size of Foley catheter used",
|
||||||
|
"value": "14 CH",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Did a surgical dissection of the bulbospongiosus muscle has been performed?",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Locking Position of the cuff around the urethra",
|
||||||
|
"value": "1",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Tightening of the urethra",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "If you have encountered difficulties, please describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": "no difficulties",
|
||||||
|
"valueClass": "valeur ENCOUNT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comment",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All steps for the \"Surgery Test procedure\" have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All steps for the \"Implantation of the control unit\" have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "All steps for the \"Completion of implantation\" have been completed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Describe the difficulty(ies) experienced and alternative or solution provided",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Comments",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Was a Picture of the Cuff final position around the urethra taken intra-operatively",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Was a Picture of the Control Unit final position taken intra-operatively",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Was the Pelvis radiography performed?",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Date of most recent pelvis radiography",
|
||||||
|
"value": "04/03/2024",
|
||||||
|
"valueClass": "valeur DTEPEL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Was the Usability Questionnaire Surgeon (intra-operative) performed",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Explain why Usability Questionnaire Surgeon was not performed",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1. The implantation of Artus® is technically simple",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2. The handling of the shell screwed on the end of the transmission cable is easy",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3. The cuff is easy to insert around the urethra",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4. Positioning the Cuff around the urethra is easy to perform",
|
||||||
|
"value": "3 : Not sure",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "5. The Cuff loop around the urethra is easy to adjust",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "6. The implantation technique of the Control Unit in the abdominal wall is easy to perform",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "7. The fixing technique for the Control Unit in the abdominal wall is easy to perform",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "8. The connection of the transmission cable and the Control Unit is easy to perform",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "9. The remote control is easy to manipulate in the sterile bag",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "10. The sequence to be carried out to reach the screen proposing the password to access the Physician interface is simple",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "11. Matching between the Remote Control and the implanted Control Unit is easy to perform",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "12. Visual evaluation of the correct operation of the device (Surgery mode) is easy to perform",
|
||||||
|
"value": "2 : Agree",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Do you have any comments regarding the intra-operative ARTUS implant and its use",
|
||||||
|
"value": "No",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Please complete your comment below",
|
||||||
|
"value": null,
|
||||||
|
"valueClass": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Device deficiency(ies) occurred during the procedure",
|
||||||
|
"value": "No",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Is the subject receiving any concomitant medication",
|
||||||
|
"value": "Yes",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Did new adverse event(s) occur since the informed consent signature",
|
||||||
|
"value": "No",
|
||||||
|
"valueClass": "valeur "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"cookies": [{"name": "i18n_lang", "value": "en", "domain": "prod.evamed.com", "path": "/etude/soft", "expires": 1810975810.980863, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "browser_compat", "value": "true", "domain": "prod.evamed.com", "path": "/etude/soft", "expires": 1779958212, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "isMobileDevice", "value": "n", "domain": "prod.evamed.com", "path": "/etude/soft", "expires": 1779958212, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "Evamed_RepClient", "value": "myopowers-dry", "domain": "prod.evamed.com", "path": "/etude/soft", "expires": 1810975813.361198, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "AreCookiesEnabled", "value": "286", "domain": "prod.evamed.com", "path": "/etude/soft", "expires": 1779958212, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "EE_Index", "value": "o54t426bm3kaktihcmsi7mrerf", "domain": "prod.evamed.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": []}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
Surgery (V0 Implantation Visit) Import — scrapes from EvaMed DRY study and upserts into MongoDB.
|
||||||
|
|
||||||
|
Run repeatedly; only stores field-level changes (delta) in history[].
|
||||||
|
Unique key: _form_id (each form has a unique ID in EvaMed).
|
||||||
|
MongoDB: db=Dry, collection=Surgery
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
BASE_URL = "https://prod.evamed.com/etude/soft/index.php"
|
||||||
|
LOGIN_URL = f"{BASE_URL}?module=authentification&class=login&client=myopowers-dry"
|
||||||
|
LIST_URL = f"{BASE_URL}?module=monitoring&class=formslisting¢er_id=2&formtype=10&l=ALL"
|
||||||
|
LOGIN = "vbuzalka"
|
||||||
|
PASSWORD = "Vlado9674+"
|
||||||
|
|
||||||
|
MONGO_HOST = "192.168.1.76"
|
||||||
|
DB_NAME = "Dry"
|
||||||
|
COLLECTION = "Surgery"
|
||||||
|
SESSION_FILE = Path(__file__).parent / "session.json"
|
||||||
|
|
||||||
|
DATE_RE = re.compile(r"^(\d{2})/(\d{2})/(\d{4})$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(value):
|
||||||
|
"""Parse DD/MM/YYYY -> datetime, digit-only -> int, else str. None if empty."""
|
||||||
|
if not value or not value.strip():
|
||||||
|
return None
|
||||||
|
v = value.strip()
|
||||||
|
m = DATE_RE.fullmatch(v)
|
||||||
|
if m:
|
||||||
|
return datetime(int(m.group(3)), int(m.group(2)), int(m.group(1)))
|
||||||
|
if re.fullmatch(r"\d+", v):
|
||||||
|
return int(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
async def do_login(page):
|
||||||
|
await page.goto(LOGIN_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await page.locator("#login").fill(LOGIN)
|
||||||
|
await page.locator('input[type="password"]').first.fill(PASSWORD)
|
||||||
|
await page.click('input[value="Connection"]')
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_form_ids(page):
|
||||||
|
"""Return list of {formId, patientCode} from the filtered forms list."""
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
return await page.evaluate("""() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('a[title="Open form"]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const m = href.match(/id=(\\d+)/);
|
||||||
|
if (!m) return;
|
||||||
|
const row = a.closest('tr');
|
||||||
|
const dirLink = row ? row.querySelector('a[title="Open directory"]') : null;
|
||||||
|
const patientCode = dirLink ? dirLink.innerText.trim() : '';
|
||||||
|
results.push({ formId: m[1], patientCode: patientCode });
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_form_fields(page, form_id):
|
||||||
|
"""Navigate to form and extract all field values."""
|
||||||
|
url = f"{BASE_URL}?module=dossier&class=file&event=show&id={form_id}#fiche"
|
||||||
|
await page.goto(url)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
||||||
|
raw = await page.evaluate("""() => {
|
||||||
|
const fields = {};
|
||||||
|
document.querySelectorAll('.tableauFormulaire span.label').forEach(label => {
|
||||||
|
const key = label.innerText.trim();
|
||||||
|
const valEl = label.nextElementSibling;
|
||||||
|
fields[key] = valEl ? valEl.innerText.trim() || null : null;
|
||||||
|
});
|
||||||
|
return fields;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
for k, v in raw.items():
|
||||||
|
parsed[k] = parse_value(v)
|
||||||
|
|
||||||
|
parsed['_form_id'] = int(form_id)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def upsert(collection, doc, patient_code, now):
|
||||||
|
form_id = doc['_form_id']
|
||||||
|
key = {"_form_id": form_id}
|
||||||
|
existing = collection.find_one(key)
|
||||||
|
|
||||||
|
skip = {'_form_id'}
|
||||||
|
data = {k: v for k, v in doc.items() if k not in skip}
|
||||||
|
|
||||||
|
if existing is None:
|
||||||
|
collection.insert_one({
|
||||||
|
**key,
|
||||||
|
"patient_code": patient_code,
|
||||||
|
"data": data,
|
||||||
|
"history": [],
|
||||||
|
"first_seen_at": now,
|
||||||
|
"last_seen_at": now,
|
||||||
|
"deleted_at": None,
|
||||||
|
})
|
||||||
|
print(f" NEW {patient_code} Surgery form_id={form_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
old_data = existing.get("data", {})
|
||||||
|
changes = {}
|
||||||
|
for k in set(data) | set(old_data):
|
||||||
|
old_v = old_data.get(k)
|
||||||
|
new_v = data.get(k)
|
||||||
|
if old_v != new_v:
|
||||||
|
changes[k] = {"old": old_v, "new": new_v}
|
||||||
|
|
||||||
|
update = {"$set": {"last_seen_at": now, "deleted_at": None, "patient_code": patient_code}}
|
||||||
|
if changes:
|
||||||
|
update["$set"]["data"] = data
|
||||||
|
update["$push"] = {"history": {"timestamp": now, "changes": changes}}
|
||||||
|
print(f" CHANGED {patient_code} Surgery form_id={form_id} -> {list(changes.keys())}")
|
||||||
|
else:
|
||||||
|
print(f" ok {patient_code} Surgery form_id={form_id}")
|
||||||
|
|
||||||
|
collection.update_one(key, update)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
mongo = MongoClient(MONGO_HOST)
|
||||||
|
col = mongo[DB_NAME][COLLECTION]
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
if SESSION_FILE.exists():
|
||||||
|
context = await browser.new_context(storage_state=str(SESSION_FILE))
|
||||||
|
print("Loaded saved session")
|
||||||
|
else:
|
||||||
|
context = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
await page.goto(LIST_URL)
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
if "authentification" in page.url:
|
||||||
|
print("Logging in...")
|
||||||
|
await do_login(page)
|
||||||
|
await context.storage_state(path=str(SESSION_FILE))
|
||||||
|
print("Session saved")
|
||||||
|
else:
|
||||||
|
print("Session valid")
|
||||||
|
|
||||||
|
form_infos = await get_form_ids(page)
|
||||||
|
current_ids = {info['formId'] for info in form_infos}
|
||||||
|
print(f"Found {len(form_infos)} Surgery forms")
|
||||||
|
|
||||||
|
for info in form_infos:
|
||||||
|
fid = info['formId']
|
||||||
|
print(f"Scraping form {fid} ({info['patientCode']})...")
|
||||||
|
doc = await extract_form_fields(page, fid)
|
||||||
|
upsert(col, doc, info['patientCode'], now)
|
||||||
|
|
||||||
|
for rec in col.find({"deleted_at": None}, {"_form_id": 1, "patient_code": 1}):
|
||||||
|
if str(rec.get('_form_id', '')) not in current_ids:
|
||||||
|
col.update_one({"_id": rec["_id"]}, {"$set": {"deleted_at": now}})
|
||||||
|
print(f" DELETED form_id={rec['_form_id']} ({rec.get('patient_code')})")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
mongo.close()
|
||||||
|
print(f"\nDone -- {len(form_infos)} forms processed at {now.isoformat()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# Název: janssenpc_file_send.py
|
||||||
|
# Verze: 2.0
|
||||||
|
# Datum: 2026-05-27
|
||||||
|
# Popis: Přejmenuje soubory ve složce ##JNJPrenos, odešle je na msgs.buzalka.cz
|
||||||
|
# a přesune do podsložky Trash. Loguje průběh do file_send.log vedle skriptu.
|
||||||
|
# Podporuje: Panorama Dashboard (xlsx), Site Visit Report (xlsx),
|
||||||
|
# Follow-Up Letter (xlsx), Clario MayoScore (csv), Clario MayoDiary (csv).
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
TOKEN = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340"
|
||||||
|
UPLOAD_URL = "https://msgs.buzalka.cz/upload-dropbox"
|
||||||
|
SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos")
|
||||||
|
TRASH_DIR = SOURCE_DIR / "Trash"
|
||||||
|
LOG_FILE = Path(__file__).parent / "file_send.log"
|
||||||
|
|
||||||
|
MAYO_DIARY_COLUMNS = [
|
||||||
|
'Protocol', 'Country', 'Site', 'PI Name', 'Subject ID',
|
||||||
|
'Report Date', 'Report Start Date/Time', 'Report End Date/Time',
|
||||||
|
'Stool Frequency', 'Form Number', 'Role', 'Original Source',
|
||||||
|
]
|
||||||
|
|
||||||
|
MAYO_SCORE_COLUMNS = [
|
||||||
|
'Protocol', 'Study Population', 'Country', 'Site', 'Principal Investigator',
|
||||||
|
'Participant ID', 'Baseline Stool Frequency', 'Visit', 'Visit Date',
|
||||||
|
'Endoscopy Completed?', 'Central Endoscopy Score', 'Local Endoscopy Score',
|
||||||
|
'Partial Mayo Score', 'Full Mayo Score',
|
||||||
|
]
|
||||||
|
|
||||||
|
PANORAMA_COLUMNS = [
|
||||||
|
'Part', 'Source', 'Sector', 'TA', 'Protocol ID', 'Interventional',
|
||||||
|
'Region', 'Country Name', 'Institution Name', 'Site City',
|
||||||
|
'Site Zip/Postal Code', 'Site Address', 'MSID', 'Site ID',
|
||||||
|
'Site Status', 'SM Full Name', 'PI Name', 'St F Subj Enr Act',
|
||||||
|
'ID', 'Category', 'Type', 'Priority', 'Severity', 'Description',
|
||||||
|
'Brief Description - Subject ID', 'Comments', 'Created By',
|
||||||
|
'Create Date', 'Last Modified Date', 'Start Date', 'Due Date',
|
||||||
|
'End Date', 'Status', 'Days Outstanding', 'Action Taken',
|
||||||
|
'Escalated To', 'Visit Report Status', 'Visit Report Approved',
|
||||||
|
'Visit Report Type', 'Visit Report Status End Date', 'Active',
|
||||||
|
'Association', 'Deviation', 'Deviation Closed Date', 'Reason For Exclusion'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
line = f"[{ts}] {msg}"
|
||||||
|
print(line)
|
||||||
|
with LOG_FILE.open("a", encoding="utf-8") as lf:
|
||||||
|
lf.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def move_to_trash(f: Path):
|
||||||
|
TRASH_DIR.mkdir(exist_ok=True)
|
||||||
|
dest = TRASH_DIR / f.name
|
||||||
|
if dest.exists():
|
||||||
|
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
dest = TRASH_DIR / f"{f.stem}_{ts}{f.suffix}"
|
||||||
|
shutil.move(str(f), dest)
|
||||||
|
|
||||||
|
|
||||||
|
def get_timestamp(file_path: str) -> str:
|
||||||
|
return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
|
||||||
|
|
||||||
|
def prejmenuj(directory: Path) -> None:
|
||||||
|
log(f"--- Přejmenování, adresář: {directory} ---")
|
||||||
|
files = [f for f in directory.iterdir() if f.is_file()]
|
||||||
|
log(f" Nalezeno souborů: {len(files)} — {[f.name for f in files]}")
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
filename = f.name
|
||||||
|
file_path = str(f)
|
||||||
|
|
||||||
|
# 0a. CLARIO MAYO DIARY (CSV)
|
||||||
|
if 'MAYO-DIARY' in filename and filename.endswith('.csv'):
|
||||||
|
log(f" Detekován MayoDiary: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
missing = set(MAYO_DIARY_COLUMNS) - set(df.columns)
|
||||||
|
if not missing:
|
||||||
|
protocols = df['Protocol'].dropna().unique()
|
||||||
|
log(f" Protocol: {list(protocols)}")
|
||||||
|
if len(protocols) > 0:
|
||||||
|
study = str(protocols[0]).strip()
|
||||||
|
new_name = f"{get_timestamp(file_path)} {study} Clario MayoDiary.csv"
|
||||||
|
f.rename(directory / new_name)
|
||||||
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 0b. CLARIO MAYO SCORE (CSV)
|
||||||
|
if 'Custom.MayoScoreReport' in filename and filename.endswith('.csv'):
|
||||||
|
log(f" Detekován MayoScore: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
missing = set(MAYO_SCORE_COLUMNS) - set(df.columns)
|
||||||
|
if not missing:
|
||||||
|
protocols = df['Protocol'].dropna().unique()
|
||||||
|
log(f" Protocol: {list(protocols)}")
|
||||||
|
if len(protocols) > 0:
|
||||||
|
study = str(protocols[0]).strip()
|
||||||
|
new_name = f"{get_timestamp(file_path)} {study} Clario MayoScore.csv"
|
||||||
|
f.rename(directory / new_name)
|
||||||
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ostatní — jen xlsx
|
||||||
|
if not filename.endswith('.xlsx'):
|
||||||
|
log(f" Přeskočeno (neznámý typ): {filename}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. PANORAMA DASHBOARD (XLSX)
|
||||||
|
if 'Panorama Dashboard' in filename:
|
||||||
|
log(f" Detekován Panorama: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(file_path, skiprows=5)
|
||||||
|
missing = set(PANORAMA_COLUMNS) - set(df.columns)
|
||||||
|
if not missing:
|
||||||
|
ids = df['Protocol ID'].dropna().unique()
|
||||||
|
log(f" Protocol ID: {list(ids)}")
|
||||||
|
if len(ids) > 0:
|
||||||
|
study = str(ids[0]).strip()
|
||||||
|
new_name = f"{get_timestamp(file_path)} {study} Panorama Deviations and Issues.xlsx"
|
||||||
|
f.rename(directory / new_name)
|
||||||
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Protocol ID je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. SITE VISIT REPORT A FOLLOW-UP LETTER (XLSX)
|
||||||
|
try:
|
||||||
|
df_a1 = pd.read_excel(file_path, nrows=1, header=None)
|
||||||
|
if not df_a1.empty:
|
||||||
|
a1 = str(df_a1.iloc[0, 0])
|
||||||
|
log(f" A1: {a1[:80]}")
|
||||||
|
is_site_visit = "Title: Site Visit Report Details" in a1
|
||||||
|
is_follow_up = "Title: Follow-Up Letter Details" in a1
|
||||||
|
|
||||||
|
if is_site_visit or is_follow_up:
|
||||||
|
suffix = "Site Visit Details.xlsx" if is_site_visit else "FUL details.xlsx"
|
||||||
|
log(f" Detekován {'Site Visit' if is_site_visit else 'Follow-Up Letter'}: {filename}")
|
||||||
|
df = pd.read_excel(file_path, skiprows=5)
|
||||||
|
if 'Protocol ID' in df.columns:
|
||||||
|
ids = df['Protocol ID'].dropna().unique()
|
||||||
|
log(f" Protocol ID: {list(ids)}")
|
||||||
|
if len(ids) > 0:
|
||||||
|
study = str(ids[0]).strip()
|
||||||
|
new_name = f"{get_timestamp(file_path)} {study} {suffix}"
|
||||||
|
f.rename(directory / new_name)
|
||||||
|
log(f" ÚSPĚCH: -> '{new_name}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Protocol ID je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupec Protocol ID.")
|
||||||
|
else:
|
||||||
|
log(f" Přeskočeno (neznámý xlsx obsah): {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
|
||||||
|
log("--- Přejmenování dokončeno ---")
|
||||||
|
|
||||||
|
|
||||||
|
# === HLAVNÍ LOGIKA ===
|
||||||
|
|
||||||
|
log("=== Spuštění ===")
|
||||||
|
log(f"Zdrojový adresář: {SOURCE_DIR} (existuje: {SOURCE_DIR.exists()})")
|
||||||
|
|
||||||
|
# 1. Přejmenuj
|
||||||
|
prejmenuj(SOURCE_DIR)
|
||||||
|
|
||||||
|
# 2. Počkej 10 vteřin
|
||||||
|
log("Čekám 10 vteřin...")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# 3. Odešli soubory
|
||||||
|
files = [f for f in SOURCE_DIR.iterdir() if f.is_file()]
|
||||||
|
log(f"Souborů k odeslání: {len(files)}")
|
||||||
|
for f in files:
|
||||||
|
log(f" Nalezen: {f.name}")
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
log("Žádné soubory k odeslání.")
|
||||||
|
else:
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
with f.open("rb") as fh:
|
||||||
|
resp = requests.post(
|
||||||
|
UPLOAD_URL,
|
||||||
|
headers={"Authorization": f"Bearer {TOKEN}"},
|
||||||
|
files={"file": (f.name, fh, "application/octet-stream")},
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
status = resp.json().get('status', '?').upper()
|
||||||
|
log(f" {status:10} | {f.name}")
|
||||||
|
move_to_trash(f)
|
||||||
|
log(f" PŘESUNUTO | {f.name} -> Trash")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA | {f.name} | {e}")
|
||||||
|
|
||||||
|
log("=== Hotovo ===")
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Název: janssenpc_file_watch.py
|
||||||
|
# Verze: 1.1
|
||||||
|
# Datum: 2026-05-27
|
||||||
|
# Popis: Démon hlídající složku ##JNJPrenos (watchdog). Při objevení nového souboru
|
||||||
|
# spustí janssenpc_file_send.py, který zajistí přejmenování, upload a přesun do Trash.
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos")
|
||||||
|
SEND_SCRIPT = Path(__file__).parent / "janssenpc_file_send.py"
|
||||||
|
|
||||||
|
|
||||||
|
def run_send():
|
||||||
|
subprocess.run([sys.executable, str(SEND_SCRIPT)], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
class NewFileHandler(FileSystemEventHandler):
|
||||||
|
def on_created(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
run_send()
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
run_send()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Při startu zpracuj soubory, které už tam jsou
|
||||||
|
if any(f for f in SOURCE_DIR.iterdir() if f.is_file()):
|
||||||
|
run_send()
|
||||||
|
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(NewFileHandler(), str(SOURCE_DIR), recursive=False)
|
||||||
|
observer.start()
|
||||||
|
print(f"Hlídám: {SOURCE_DIR}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Název: 02 PřejmenujSouboryReportu.py
|
||||||
|
# Verze: 1.2
|
||||||
|
# Datum: 2026-05-27
|
||||||
|
# Popis: Prochází zadaný adresář a přejmenuje známé typy reportů na standardizovaný
|
||||||
|
# formát "datum čas studie typ.přípona". Podporuje: Panorama Dashboard (xlsx),
|
||||||
|
# Site Visit Report (xlsx), Follow-Up Letter (xlsx),
|
||||||
|
# Clario MayoScore (csv), Clario MayoDiary (csv).
|
||||||
|
# Loguje průběh do prejmenuj.log vedle skriptu.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOG_FILE = Path(__file__).parent / "prejmenuj.log"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
line = f"[{ts}] {msg}"
|
||||||
|
print(line)
|
||||||
|
with LOG_FILE.open("a", encoding="utf-8") as lf:
|
||||||
|
lf.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def zpracuj_reporty(directory_path):
|
||||||
|
mayo_diary_columns = [
|
||||||
|
'Protocol', 'Country', 'Site', 'PI Name', 'Subject ID',
|
||||||
|
'Report Date', 'Report Start Date/Time', 'Report End Date/Time',
|
||||||
|
'Stool Frequency', 'Form Number', 'Role', 'Original Source',
|
||||||
|
]
|
||||||
|
|
||||||
|
mayo_columns = [
|
||||||
|
'Protocol', 'Study Population', 'Country', 'Site', 'Principal Investigator',
|
||||||
|
'Participant ID', 'Baseline Stool Frequency', 'Visit', 'Visit Date',
|
||||||
|
'Endoscopy Completed?', 'Central Endoscopy Score', 'Local Endoscopy Score',
|
||||||
|
'Partial Mayo Score', 'Full Mayo Score',
|
||||||
|
]
|
||||||
|
|
||||||
|
panorama_columns = [
|
||||||
|
'Part', 'Source', 'Sector', 'TA', 'Protocol ID', 'Interventional',
|
||||||
|
'Region', 'Country Name', 'Institution Name', 'Site City',
|
||||||
|
'Site Zip/Postal Code', 'Site Address', 'MSID', 'Site ID',
|
||||||
|
'Site Status', 'SM Full Name', 'PI Name', 'St F Subj Enr Act',
|
||||||
|
'ID', 'Category', 'Type', 'Priority', 'Severity', 'Description',
|
||||||
|
'Brief Description - Subject ID', 'Comments', 'Created By',
|
||||||
|
'Create Date', 'Last Modified Date', 'Start Date', 'Due Date',
|
||||||
|
'End Date', 'Status', 'Days Outstanding', 'Action Taken',
|
||||||
|
'Escalated To', 'Visit Report Status', 'Visit Report Approved',
|
||||||
|
'Visit Report Type', 'Visit Report Status End Date', 'Active',
|
||||||
|
'Association', 'Deviation', 'Deviation Closed Date', 'Reason For Exclusion'
|
||||||
|
]
|
||||||
|
|
||||||
|
log(f"=== Spuštění přejmenování, adresář: {directory_path} ===")
|
||||||
|
|
||||||
|
if not os.path.exists(directory_path):
|
||||||
|
log(f"CHYBA: Adresář '{directory_path}' neexistuje.")
|
||||||
|
return
|
||||||
|
|
||||||
|
all_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
|
log(f"Nalezeno souborů: {len(all_files)} — {all_files}")
|
||||||
|
|
||||||
|
for filename in all_files:
|
||||||
|
file_path = os.path.join(directory_path, filename)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 0a. CLARIO MAYO DIARY (CSV)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if 'MAYO-DIARY' in filename and filename.endswith('.csv'):
|
||||||
|
log(f"Detekován MayoDiary: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
actual_columns = set(df.columns)
|
||||||
|
missing = set(mayo_diary_columns) - actual_columns
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
protocols = df['Protocol'].dropna().unique()
|
||||||
|
log(f" Protocol hodnoty: {list(protocols)}")
|
||||||
|
if len(protocols) > 0:
|
||||||
|
study_name = str(protocols[0]).strip()
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||||
|
timestamp = file_time.strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
new_filename = f"{timestamp} {study_name} Clario MayoDiary.csv"
|
||||||
|
os.rename(file_path, os.path.join(directory_path, new_filename))
|
||||||
|
log(f" ÚSPĚCH: -> '{new_filename}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 0b. CLARIO MAYO SCORE (CSV)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if 'Custom.MayoScoreReport' in filename and filename.endswith('.csv'):
|
||||||
|
log(f"Detekován MayoScore: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
actual_columns = set(df.columns)
|
||||||
|
missing = set(mayo_columns) - actual_columns
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
protocols = df['Protocol'].dropna().unique()
|
||||||
|
log(f" Protocol hodnoty: {list(protocols)}")
|
||||||
|
if len(protocols) > 0:
|
||||||
|
study_name = str(protocols[0]).strip()
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||||
|
timestamp = file_time.strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
new_filename = f"{timestamp} {study_name} Clario MayoScore.csv"
|
||||||
|
os.rename(file_path, os.path.join(directory_path, new_filename))
|
||||||
|
log(f" ÚSPĚCH: -> '{new_filename}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ostatní typy — jen xlsx
|
||||||
|
if not filename.endswith('.xlsx'):
|
||||||
|
log(f"Přeskočeno (neznámý typ): {filename}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 1. PANORAMA DASHBOARD (XLSX)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if 'Panorama Dashboard' in filename:
|
||||||
|
log(f"Detekován Panorama: {filename}")
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(file_path, skiprows=5)
|
||||||
|
actual_columns = set(df.columns)
|
||||||
|
missing = set(panorama_columns) - actual_columns
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
protocol_ids = df['Protocol ID'].dropna().unique()
|
||||||
|
log(f" Protocol ID hodnoty: {list(protocol_ids)}")
|
||||||
|
if len(protocol_ids) > 0:
|
||||||
|
study_name = str(protocol_ids[0]).strip()
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||||
|
timestamp = file_time.strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
new_filename = f"{timestamp} {study_name} Panorama Deviations and Issues.xlsx"
|
||||||
|
os.rename(file_path, os.path.join(directory_path, new_filename))
|
||||||
|
log(f" ÚSPĚCH: -> '{new_filename}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol ID je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Chybí sloupce: {missing}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 2. SITE VISIT REPORT A FOLLOW-UP LETTER (XLSX)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
df_a1 = pd.read_excel(file_path, nrows=1, header=None)
|
||||||
|
if not df_a1.empty:
|
||||||
|
a1_text = str(df_a1.iloc[0, 0])
|
||||||
|
log(f" A1 obsah: {a1_text[:80]}")
|
||||||
|
|
||||||
|
is_site_visit = "Title: Site Visit Report Details" in a1_text
|
||||||
|
is_follow_up = "Title: Follow-Up Letter Details" in a1_text
|
||||||
|
|
||||||
|
if is_site_visit or is_follow_up:
|
||||||
|
suffix = "Site Visit Details.xlsx" if is_site_visit else "FUL details.xlsx"
|
||||||
|
log(f"Detekován {'Site Visit' if is_site_visit else 'Follow-Up Letter'}: {filename}")
|
||||||
|
|
||||||
|
df = pd.read_excel(file_path, skiprows=5)
|
||||||
|
if 'Protocol ID' in df.columns:
|
||||||
|
protocol_ids = df['Protocol ID'].dropna().unique()
|
||||||
|
log(f" Protocol ID hodnoty: {list(protocol_ids)}")
|
||||||
|
if len(protocol_ids) > 0:
|
||||||
|
study_name = str(protocol_ids[0]).strip()
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||||
|
timestamp = file_time.strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
new_filename = f"{timestamp} {study_name} {suffix}"
|
||||||
|
os.rename(file_path, os.path.join(directory_path, new_filename))
|
||||||
|
log(f" ÚSPĚCH: -> '{new_filename}'")
|
||||||
|
else:
|
||||||
|
log(f" VAROVÁNÍ: Sloupec Protocol ID je prázdný.")
|
||||||
|
else:
|
||||||
|
log(f" PŘESKOČENO: Soubor neobsahuje sloupec 'Protocol ID'.")
|
||||||
|
else:
|
||||||
|
log(f"Přeskočeno (neznámý xlsx obsah): {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" CHYBA: {e}")
|
||||||
|
|
||||||
|
log("=== Přejmenování dokončeno ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cesta_k_adresari = r"c:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos"
|
||||||
|
zpracuj_reporty(cesta_k_adresari)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"Protocol","Study Population","Country","Site","Principal Investigator","Participant ID","Baseline Stool Frequency","Visit","Visit Date","Endoscopy Completed?","Endoscopy Date","Bowel Preparation Start Date 1","Bowel Preparation End Date 1","Bowel Preparation Start Date 2","Bowel Preparation End Date 2","Central Endoscopy Score","Local Endoscopy Score","PGA Score","Eligible Day (-1)","Day (-1) Excluded Reason(s)","Eligible Day (-2)","Day (-2) Excluded Reason(s)","Eligible Day (-3)","Day (-3) Excluded Reason(s)","Eligible Day (-4)","Day (-4) Excluded Reason(s)","Eligible Day (-5)","Day (-5) Excluded Reason(s)","Eligible Day (-6)","Day (-6) Excluded Reason(s)","Eligible Day (-7)","Day (-7) Excluded Reason(s)","Eligible Day (-8)","Day (-8) Excluded Reason(s)","Eligible Day (-9)","Day (-9) Excluded Reason(s)","Eligible Day (-10)","Day (-10) Excluded Reason(s)","Eligible Day (-1) Stool Count","Eligible Day (-2) Stool Count","Eligible Day (-3) Stool Count","Eligible Day (-4) Stool Count","Eligible Day (-5) Stool Count","Eligible Day (-6) Stool Count","Eligible Day (-7) Stool Count","Eligible Day (-8) Stool Count","Eligible Day (-9) Stool Count","Eligible Day (-10) Stool Count","Stool Frequency Sub-score","Eligible Day (-1) Rectal Bleeding Score","Eligible Day (-2) Rectal Bleeding Score","Eligible Day (-3) Rectal Bleeding Score","Eligible Day (-4) Rectal Bleeding Score","Eligible Day (-5) Rectal Bleeding Score","Eligible Day (-6) Rectal Bleeding Score","Eligible Day (-7) Rectal Bleeding Score","Eligible Day (-8) Rectal Bleeding Score","Eligible Day (-9) Rectal Bleeding Score","Eligible Day (-10) Rectal Bleeding Score","Rectal Bleeding Sub-score","Partial Mayo Score","Modified Mayo Score","Full Mayo Score","Site Action","Last Mayo Score Submission","Week I-12 Clinical Responder","Week I-12 Clinical Remission","Clinical Flare","Loss of Response","Partial Mayo Response Post Loss of Response","Partial Mayo Response for Clinical Non-Responders"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-0","19 Feb 2026","Yes","05 Feb 2026","04 Feb 2026","04 Feb 2026","-","-","2","-","3","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Day Not Applicable for Calculation","10 Feb 2026","Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","10","8","7","5","7","8","8","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","7","6","9","-","08 Apr 2026 07:11:25","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-2","04 Mar 2026","-","-","-","-","-","-","-","-","3","03 Mar 2026","-","02 Mar 2026","-","01 Mar 2026","-","28 Feb 2026","-","27 Feb 2026","-","26 Feb 2026","-","25 Feb 2026","-","24 Feb 2026","Day Not Applicable for Calculation","23 Feb 2026","Day Not Applicable for Calculation","22 Feb 2026","Day Not Applicable for Calculation","5","4","5","4","5","6","6","-","-","-","2","1","0","1","0","1","0","1","-","-","-","1","6","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-4","18 Mar 2026","-","-","-","-","-","-","-","-","2","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","-","11 Mar 2026","-","10 Mar 2026","Day Not Applicable for Calculation","09 Mar 2026","Day Not Applicable for Calculation","08 Mar 2026","Day Not Applicable for Calculation","5","5","5","4","5","4","5","-","-","-","2","1","0","0","1","1","1","0","-","-","-","1","5","","","-","08 Apr 2026 11:04:49","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-8","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","4","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-12","13 May 2026","Yes","06 May 2026","05 May 2026","05 May 2026","-","-","1","-","1","12 May 2026","-","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","Endoscopy","05 May 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","04 May 2026","-","03 May 2026","Day Not Applicable for Calculation","5","4","6","5","5","5","-","-","3","-","2","1","0","1","1","1","1","-","-","1","-","1","4","4","5","-","-","Clinical Responder","No","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-0","08 Apr 2026","Yes","18 Mar 2026","17 Mar 2026","18 Mar 2026","-","-","2","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","Missing Diary","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","-","3","3","4","-","-","-","1","0","0","0","-","0","0","1","-","-","-","0","3","3","5","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-2","23 Apr 2026","-","-","-","-","-","-","-","-","2","22 Apr 2026","Missing Diary","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","Day Not Applicable for Calculation","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","-","3","3","6","5","5","4","-","-","-","2","-","0","0","1","1","1","1","-","-","-","1","5","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-4","06 May 2026","-","-","-","-","-","-","-","-","1","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","6","3","2","3","3","3","3","-","-","-","1","1","0","0","0","1","1","0","-","-","-","0","2","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","1","I-0","27 May 2026","Yes","13 May 2026","12 May 2026","12 May 2026","-","-","3","-","2","26 May 2026","-","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","Day Not Applicable for Calculation","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","6","9","7","8","9","7","8","-","-","-","3","2","2","2","2","1","1","1","-","-","-","2","7","8","10","-","27 May 2026 07:24:39","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-0","20 Mar 2026","Yes","19 Feb 2026","-","-","-","-","3","-","3","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","Day Not Applicable for Calculation","11 Mar 2026","Day Not Applicable for Calculation","10 Mar 2026","Day Not Applicable for Calculation","7","7","8","8","7","8","5","-","-","-","3","2","1","1","1","1","1","0","-","-","-","1","7","7","10","-","20 Mar 2026 07:03:23","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","Medication For Diarrhea","06 Apr 2026","Medication For Diarrhea","05 Apr 2026","Medication For Diarrhea","04 Apr 2026","Medication For Diarrhea","03 Apr 2026","Medication For Diarrhea","02 Apr 2026","Medication For Diarrhea","01 Apr 2026","Medication For Diarrhea","31 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","30 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","Non-Evaluable","Non-Evaluable","Non-Evaluable","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-4","15 Apr 2026","-","-","-","-","-","-","-","-","3","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","-","11 Apr 2026","-","10 Apr 2026","-","09 Apr 2026","-","08 Apr 2026","-","07 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","06 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","05 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","9","22","20","19","17","18","18","-","-","-","3","1","3","2","2","2","2","2","-","-","-","2","8","","","-","04 May 2026 22:06:03","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-8","18 May 2026","-","-","-","-","-","-","-","-","2","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","7","5","9","7","7","8","8","-","-","-","3","1","1","1","1","1","1","1","-","-","-","1","6","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","1","I-0","26 May 2026","Yes","14 May 2026","13 May 2026","13 May 2026","-","-","2","-","2","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","8","8","6","7","7","6","7","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","7","7","9","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-0","05 May 2026","Yes","24 Apr 2026","23 Apr 2026","23 Apr 2026","-","-","2","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","5","5","5","5","5","5","5","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","5","5","7","-","05 May 2026 11:19:40","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-2","19 May 2026","-","-","-","-","-","-","-","-","1","18 May 2026","-","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","Day Not Applicable for Calculation","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","5","4","5","5","5","4","6","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","19 May 2026 10:38:25","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-0","07 Apr 2026","Yes","24 Mar 2026","22 Mar 2026","22 Mar 2026","-","-","2","-","2","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","8","11","5","9","11","10","13","-","-","-","3","1","2","2","2","2","2","2","-","-","-","2","7","7","9","-","04 May 2026 08:44:52","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","7","5","6","6","7","8","2","-","-","-","1","1","0","1","1","1","2","0","-","-","-","1","4","","","-","04 May 2026 08:45:07","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-4","07 May 2026","-","-","-","-","-","-","-","-","1","06 May 2026","-","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","Day Not Applicable for Calculation","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","8","7","7","8","4","11","7","-","-","-","1","2","1","1","1","0","1","1","-","-","-","1","3","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-0","24 Mar 2026","Yes","12 Mar 2026","11 Mar 2026","11 Mar 2026","-","-","2","-","2","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","Day Not Applicable for Calculation","15 Mar 2026","Day Not Applicable for Calculation","14 Mar 2026","Day Not Applicable for Calculation","8","6","5","7","6","7","6","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","6","6","8","-","05 Apr 2026 22:41:27","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","5","2","3","6","5","5","5","-","-","-","2","0","0","0","0","1","1","0","-","-","-","0","4","","","-","27 May 2026 12:53:52","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-4","21 Apr 2026","-","-","-","-","-","-","-","-","0","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","4","3","4","3","3","4","4","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","2","","","-","27 May 2026 12:54:41","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-0","12 May 2026","Yes","21 Apr 2026","20 Apr 2026","21 Apr 2026","-","-","2","-","2","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","-","05 May 2026","Missing Diary","04 May 2026","Day Not Applicable for Calculation","03 May 2026","Day Not Applicable for Calculation","02 May 2026","Day Not Applicable for Calculation","2","1","1","1","1","2","-","-","-","-","0","0","0","0","0","0","0","-","-","-","-","0","2","2","4","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-2","26 May 2026","-","-","-","-","-","-","-","-","1","25 May 2026","-","24 May 2026","Missing Diary","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Missing Diary;Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","1","-","1","2","1","2","2","-","-","-","1","0","-","0","0","0","0","0","-","-","-","0","2","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","Unscheduled 1","04 May 2026","Yes","20 Apr 2026","12 Apr 2026","15 Apr 2026","-","-","2","-","3","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Day Not Applicable for Calculation","5","6","6","7","6","3","3","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","5","4","7","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","I-0","18 May 2026","Yes","01 May 2026","01 May 2026","01 May 2026","-","-","2","-","3","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","6","6","6","6","6","6","6","-","-","-","3","0","0","0","0","0","0","0","-","-","-","0","6","5","8","-","18 May 2026 08:38:55","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-0","07 Apr 2026","Yes","16 Mar 2026","15 Mar 2026","16 Mar 2026","-","-","3","-","3","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","11","11","10","11","11","10","9","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","8","8","11","-","20 Apr 2026 09:27:58","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-2","20 Apr 2026","-","-","-","-","-","-","-","-","3","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","10 Apr 2026","Day Not Applicable for Calculation","8","7","9","8","8","7","8","-","-","-","3","2","2","1","1","1","2","1","-","-","-","1","7","","","-","20 Apr 2026 09:29:01","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","6","6","6","6","7","7","6","-","-","-","3","0","0","1","1","1","1","1","-","-","-","1","5","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","1","I-0","19 Feb 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Endoscopy;Bowel Preparation for Procedure;Day Not Applicable for Calculation","10 Feb 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","3","2","2","3","4","3","2","-","-","-","1","1","1","0","0","0","2","2","-","-","-","1","4","4","6","-","19 Feb 2026 15:41:35","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-0","09 Mar 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","08 Mar 2026","-","07 Mar 2026","-","06 Mar 2026","-","05 Mar 2026","-","04 Mar 2026","-","03 Mar 2026","Missing Diary","02 Mar 2026","Missing Diary","01 Mar 2026","Missing Diary;Day Not Applicable for Calculation","28 Feb 2026","Missing Diary;Day Not Applicable for Calculation","27 Feb 2026","Missing Diary;Day Not Applicable for Calculation","7","7","6","6","7","-","-","-","-","-","3","2","2","2","2","2","-","-","-","-","-","2","7","7","9","-","22 Mar 2026 18:34:58","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-2","27 Mar 2026","-","-","-","-","-","-","-","-","2","26 Mar 2026","-","25 Mar 2026","-","24 Mar 2026","-","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","Day Not Applicable for Calculation","18 Mar 2026","Day Not Applicable for Calculation","17 Mar 2026","Day Not Applicable for Calculation","7","3","3","3","5","5","5","-","-","-","2","0","0","1","1","1","1","2","-","-","-","1","5","","","-","27 Mar 2026 07:22:31","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-4","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","3","-","-","-","2","1","0","0","2","1","1","2","-","-","-","1","5","","","-","08 Apr 2026 07:59:35","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-8","04 May 2026","-","-","-","-","-","-","-","-","2","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Missing Diary;Day Not Applicable for Calculation","3","5","3","3","3","2","3","-","-","-","1","0","0","0","0","0","0","0","-","-","-","0","3","","","-","04 May 2026 07:52:47","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-0","09 Apr 2026","Yes","08 Apr 2026","31 Mar 2026","01 Apr 2026","-","-","2","-","2","08 Apr 2026","Endoscopy","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","31 Mar 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","30 Mar 2026","-","-","3","3","4","3","4","3","-","-","3","1","-","2","2","2","2","2","2","-","-","2","2","5","5","7","-","-","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","3","3","5","3","2","3","2","-","-","-","1","1","2","2","1","1","1","2","-","-","-","1","4","","","-","05 May 2026 07:29:35","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","4","2","2","2","2","2","2","-","-","-","1","1","1","1","1","2","1","1","-","-","-","1","4","","","-","05 May 2026 07:28:55","N/A","N/A","N/A","N/A","N/A","N/A"
|
||||||
|
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
EmailMessagingGraph.py
|
||||||
|
----------------------
|
||||||
|
Private Microsoft Graph mail sender
|
||||||
|
Application permissions, shared mailbox
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import msal
|
||||||
|
import requests
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union, List
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# PRIVATE CONFIG (ONLY YOU)
|
||||||
|
# =========================
|
||||||
|
TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9"
|
||||||
|
CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f"
|
||||||
|
CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk"
|
||||||
|
SENDER = "reports@buzalka.cz"
|
||||||
|
|
||||||
|
|
||||||
|
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
|
||||||
|
SCOPE = ["https://graph.microsoft.com/.default"]
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _get_token() -> str:
|
||||||
|
app = msal.ConfidentialClientApplication(
|
||||||
|
CLIENT_ID,
|
||||||
|
authority=AUTHORITY,
|
||||||
|
client_credential=CLIENT_SECRET,
|
||||||
|
)
|
||||||
|
|
||||||
|
token = app.acquire_token_for_client(scopes=SCOPE)
|
||||||
|
|
||||||
|
if "access_token" not in token:
|
||||||
|
raise RuntimeError(f"Graph auth failed: {token}")
|
||||||
|
|
||||||
|
return token["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(
|
||||||
|
to: Union[str, List[str]],
|
||||||
|
subject: str,
|
||||||
|
body: str = "",
|
||||||
|
*,
|
||||||
|
html: bool = False,
|
||||||
|
attachments: Union[str, Path, List[Union[str, Path]], None] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Send email via Microsoft Graph.
|
||||||
|
|
||||||
|
:param to: email or list of emails
|
||||||
|
:param subject: subject
|
||||||
|
:param body: email body (default empty)
|
||||||
|
:param html: True = HTML, False = plain text
|
||||||
|
:param attachments: file path or list of file paths to attach
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(to, str):
|
||||||
|
to = [to]
|
||||||
|
|
||||||
|
if attachments is None:
|
||||||
|
attachments = []
|
||||||
|
elif isinstance(attachments, (str, Path)):
|
||||||
|
attachments = [attachments]
|
||||||
|
|
||||||
|
attachment_payloads = []
|
||||||
|
for path in attachments:
|
||||||
|
path = Path(path)
|
||||||
|
attachment_payloads.append({
|
||||||
|
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||||
|
"name": path.name,
|
||||||
|
"contentType": "application/octet-stream",
|
||||||
|
"contentBytes": base64.b64encode(path.read_bytes()).decode(),
|
||||||
|
})
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"message": {
|
||||||
|
"subject": subject,
|
||||||
|
"body": {
|
||||||
|
"contentType": "HTML" if html else "Text",
|
||||||
|
"content": body,
|
||||||
|
},
|
||||||
|
"toRecipients": [
|
||||||
|
{"emailAddress": {"address": addr}} for addr in to
|
||||||
|
],
|
||||||
|
**({"attachments": attachment_payloads} if attachment_payloads else {}),
|
||||||
|
},
|
||||||
|
"saveToSentItems": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {_get_token()}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
f"https://graph.microsoft.com/v1.0/users/{SENDER}/sendMail",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code != 202:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"sendMail failed [{r.status_code}]: {r.text}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import winreg
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def get_dropbox_root() -> str:
|
||||||
|
"""
|
||||||
|
Vrátí kořenovou cestu složky Dropbox na tomto počítači.
|
||||||
|
|
||||||
|
Dropbox může být nainstalován na různých discích (C:, U:, Z: …),
|
||||||
|
ale struktura složek uvnitř zůstává vždy stejná. Tato funkce zjistí
|
||||||
|
aktuální umístění, takže ostatní skripty nemusí cestu napevno zadávat.
|
||||||
|
|
||||||
|
Postup hledání (v tomto pořadí):
|
||||||
|
1. Registr HKCU\\Software\\Dropbox\\ks — hlavní klíč, hodnota "Personal"
|
||||||
|
je uložena jako byte array v kódování UTF-16 LE.
|
||||||
|
2. Registr HKCU\\Software\\Dropbox\\ks1 — alternativní klíč používaný
|
||||||
|
novějšími verzemi klienta Dropbox.
|
||||||
|
3. Soubor info.json v %APPDATA%\\Dropbox\\ nebo %LOCALAPPDATA%\\Dropbox\\
|
||||||
|
— záložní metoda, pokud registr cestu neobsahuje.
|
||||||
|
|
||||||
|
Vrací:
|
||||||
|
str: Absolutní cesta ke kořenové složce Dropboxu, např. "U:\\Dropbox".
|
||||||
|
|
||||||
|
Vyvolá:
|
||||||
|
RuntimeError: Pokud se cestu nepodaří zjistit žádnou z metod.
|
||||||
|
|
||||||
|
Příklad použití:
|
||||||
|
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||||
|
import os
|
||||||
|
|
||||||
|
ROOT = get_dropbox_root()
|
||||||
|
PACIENTI = os.path.join(ROOT, "Ordinace", "Pacienti")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Metoda 1 a 2: registr HKCU\Software\Dropbox\ks a ks1
|
||||||
|
for subkey in (r"Software\Dropbox\ks", r"Software\Dropbox\ks1"):
|
||||||
|
try:
|
||||||
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, subkey) as key:
|
||||||
|
value, _ = winreg.QueryValueEx(key, "Personal")
|
||||||
|
path = bytes(value).decode("utf-16-le").rstrip("\x00")
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Metoda 3: záložní — info.json v AppData
|
||||||
|
for base in (os.getenv("APPDATA", ""), os.getenv("LOCALAPPDATA", "")):
|
||||||
|
info_path = os.path.join(base, "Dropbox", "info.json")
|
||||||
|
if os.path.isfile(info_path):
|
||||||
|
with open(info_path, encoding="utf-8") as f:
|
||||||
|
info = json.load(f)
|
||||||
|
path = (info.get("personal") or info.get("business") or {}).get("path", "")
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
raise RuntimeError("Nepodařilo se zjistit cestu k Dropboxu.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
root = get_dropbox_root()
|
||||||
|
print(f"Dropbox root: {root}")
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
"""
|
||||||
|
download_report.py
|
||||||
|
NAHRAZENO skriptem download_edc_datalistings.py
|
||||||
|
|
||||||
|
Původně: stahování Data Listing reportů pro studii MDD3003 (CZE).
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Stahuje Data Listing reporty (ReportID=92) pro studii UCO3001.
|
download_uco3001.py
|
||||||
Použití:
|
NAHRAZENO skriptem download_edc_datalistings.py
|
||||||
download_datalisting_reports_3001("Trial Disposition (Completion / Discontinuation)")
|
|
||||||
download_datalisting_reports_3001("Trial Disposition (Completion / Discontinuation)", country="CZE")
|
Původně: stahování Data Listing reportů (ReportID=92) pro studii UCO3001.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
"""
|
||||||
|
download_edc_datalistings.py
|
||||||
|
Verze: 2.0
|
||||||
|
Datum: 2026-05-27
|
||||||
|
|
||||||
|
Univerzální stahování EDC Data Listing reportů (ReportID=92) z Medidata Rave.
|
||||||
|
|
||||||
|
Parametry:
|
||||||
|
study – vyhledávací řetězec studie (např. "77242113UCO3001")
|
||||||
|
forms – seznam názvů formulářů ke stažení
|
||||||
|
country – kód země / site group (např. "CZE"), None = všechny
|
||||||
|
|
||||||
|
Prohlížeč se otevře jednou, přihlásí se, a stáhne všechny formuláře v jedné session.
|
||||||
|
|
||||||
|
Použití:
|
||||||
|
from download_edc import download_datalisting
|
||||||
|
|
||||||
|
download_datalisting(
|
||||||
|
study="77242113UCO3001",
|
||||||
|
forms=["Date of Visit", "Concomitant Therapy"],
|
||||||
|
country="CZE",
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
REPORT_ID = 92
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_study_label(study_search: str) -> str:
|
||||||
|
match = re.search(r'[A-Z]+\d+$', study_search)
|
||||||
|
return match.group(0) if match else study_search
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.wait_for_url("**/home.imedidata.com**", timeout=30_000)
|
||||||
|
except PWTimeout:
|
||||||
|
dbg(page, "wait-home-timeout")
|
||||||
|
|
||||||
|
if "home.imedidata.com" not in page.url:
|
||||||
|
print("CHYBA: Přihlášení se nezdařilo!")
|
||||||
|
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
|
||||||
|
|
||||||
|
for sel_el in page.query_selector_all("select"):
|
||||||
|
for opt in sel_el.query_selector_all("option"):
|
||||||
|
txt = (opt.inner_text() or "").strip()
|
||||||
|
if "site manager" in txt.lower():
|
||||||
|
sel_el.select_option(label=txt)
|
||||||
|
print(f" Vybráno: '{txt}'")
|
||||||
|
break
|
||||||
|
|
||||||
|
for btn_sel in ['input[value="Continue"]', 'input[type="submit"]',
|
||||||
|
'button:has-text("Continue")', 'button[type="submit"]']:
|
||||||
|
try:
|
||||||
|
btn = page.query_selector(btn_sel)
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
if btn:
|
||||||
|
btn.click()
|
||||||
|
wait_load(page, 2000)
|
||||||
|
break
|
||||||
|
dbg(page, "after-role")
|
||||||
|
|
||||||
|
|
||||||
|
def navigate_to_reporter(page):
|
||||||
|
print("Klikám na Reporter...")
|
||||||
|
page.wait_for_selector('a:has-text("Reporter")', timeout=15_000)
|
||||||
|
page.click('a:has-text("Reporter")')
|
||||||
|
wait_load(page, 1500)
|
||||||
|
dbg(page, "reporter")
|
||||||
|
|
||||||
|
|
||||||
|
def open_report(page):
|
||||||
|
print(f"Otevírám report ID={REPORT_ID} (Data Listing - Data Stream)...")
|
||||||
|
selector = f'a[href="PromptsPage.aspx?ReportID={REPORT_ID}"]'
|
||||||
|
page.wait_for_selector(selector, timeout=15_000)
|
||||||
|
page.click(selector)
|
||||||
|
wait_load(page, 2000)
|
||||||
|
dbg(page, "report-opened")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parametry reportu
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def set_study_param(page, study_search: str):
|
||||||
|
print(f" Parametr Study: hledám '{study_search}'...")
|
||||||
|
|
||||||
|
page.click('#PromptsBox_st_ShowHideBtn')
|
||||||
|
page.wait_for_timeout(1500)
|
||||||
|
|
||||||
|
page.wait_for_selector('input[id^="PromptsBox_st_FrontEndCBList_"]', timeout=10_000)
|
||||||
|
checkboxes = page.query_selector_all('input[id^="PromptsBox_st_FrontEndCBList_"]')
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for cb in checkboxes:
|
||||||
|
cb_id = cb.get_attribute("id")
|
||||||
|
label_text = page.evaluate(
|
||||||
|
"""id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return '';
|
||||||
|
const row = el.closest('tr') || el.closest('td') || el.parentElement;
|
||||||
|
return row ? row.innerText : '';
|
||||||
|
}""",
|
||||||
|
cb_id
|
||||||
|
)
|
||||||
|
print(f" [{cb_id}] label: {label_text.strip()[:80]}")
|
||||||
|
if study_search.upper() in label_text.upper():
|
||||||
|
if not page.locator(f"#{cb_id}").is_checked():
|
||||||
|
page.locator(f"#{cb_id}").check()
|
||||||
|
print(f" Nalezeno a zaškrtnuto: '{label_text.strip()}'")
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print(f" VAROVÁNÍ: Studie '{study_search}' nenalezena! Zkouším index 0...")
|
||||||
|
cb0 = page.locator('#PromptsBox_st_FrontEndCBList_0')
|
||||||
|
if not cb0.is_checked():
|
||||||
|
cb0.check()
|
||||||
|
|
||||||
|
wait_load(page, 3000)
|
||||||
|
dbg(page, "after-study")
|
||||||
|
|
||||||
|
|
||||||
|
def set_site_group_param(page, country: str):
|
||||||
|
print(f" Parametr Site Group: {country}")
|
||||||
|
|
||||||
|
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=country)
|
||||||
|
page.evaluate(
|
||||||
|
"document.querySelector('#PromptsBox_sg_List').dispatchEvent(new Event('change', {bubbles:true}))"
|
||||||
|
)
|
||||||
|
wait_load(page, 2000)
|
||||||
|
|
||||||
|
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_form_param(page, form_name: str):
|
||||||
|
print(f" Parametr Form: {form_name}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if page.locator('#PromptsBox_fm2_PageModeBtn').is_visible():
|
||||||
|
page.click('#PromptsBox_fm2_PageModeBtn')
|
||||||
|
page.wait_for_timeout(1000)
|
||||||
|
page.click('#PromptsBox_fm2_PageModeBtn')
|
||||||
|
page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
search = page.locator('#PromptsBox_fm2_SearchTxt')
|
||||||
|
search.wait_for(state='visible', timeout=10_000)
|
||||||
|
search.click()
|
||||||
|
search.fill(form_name)
|
||||||
|
page.wait_for_timeout(2000)
|
||||||
|
search.press('Enter')
|
||||||
|
page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
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!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not cb_locator.is_checked():
|
||||||
|
cb_locator.click()
|
||||||
|
print(f" '{form_name}' zaškrtnuto")
|
||||||
|
page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Submit a download
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def submit_and_download(page, context, form_name: str, country: str | None, study_label: str):
|
||||||
|
print("Odesílám report...")
|
||||||
|
|
||||||
|
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 (max 5 min)...")
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
break
|
||||||
|
|
||||||
|
for sel in target_frame.query_selector_all('select'):
|
||||||
|
for opt in sel.query_selector_all('option'):
|
||||||
|
val = opt.get_attribute('value') or ''
|
||||||
|
if 'vnd.ms-excel' in val:
|
||||||
|
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")
|
||||||
|
country_slug = country if country else "ALL"
|
||||||
|
form_slug = form_name.replace(" ", "").replace("/", "-").replace("(", "").replace(")", "")
|
||||||
|
filename = f"{timestamp}_EDC_{study_label}_{country_slug}_{form_slug}_DataListing.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()
|
||||||
|
|
||||||
|
dl_info.value.save_as(str(output_path))
|
||||||
|
print(f" Uloženo: {output_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_page.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hlavní funkce
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def download_datalisting(study: str, forms: list[str], country: str | None = None):
|
||||||
|
"""
|
||||||
|
Stáhne EDC Data Listing reporty pro zadanou studii.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
study: Vyhledávací řetězec studie, např. "77242113UCO3001"
|
||||||
|
forms: Seznam názvů formulářů ke stažení
|
||||||
|
country: Kód site group, např. "CZE". None = všechny země.
|
||||||
|
"""
|
||||||
|
if not PASSWORD:
|
||||||
|
print("Chyba: nastav IMEDIDATA_PASSWORD v souboru .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not forms:
|
||||||
|
print("Žádné formuláře ke stažení.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
DOWNLOAD_DIR.mkdir(exist_ok=True)
|
||||||
|
study_label = extract_study_label(study)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
|
||||||
|
prompts_url = page.url
|
||||||
|
|
||||||
|
print("\nNastavuji parametry reportu...")
|
||||||
|
set_study_param(page, study)
|
||||||
|
|
||||||
|
if country:
|
||||||
|
set_site_group_param(page, country)
|
||||||
|
else:
|
||||||
|
print(" Parametr Site Group: přeskočen (všechny země)")
|
||||||
|
|
||||||
|
for i, form_name in enumerate(forms):
|
||||||
|
print(f"\n=== [{i+1}/{len(forms)}] Stahuji formulář: {form_name} ===")
|
||||||
|
|
||||||
|
if i > 0:
|
||||||
|
print("Navigace zpět na report...")
|
||||||
|
page.goto(prompts_url)
|
||||||
|
wait_load(page, 2000)
|
||||||
|
set_study_param(page, study)
|
||||||
|
if country:
|
||||||
|
set_site_group_param(page, country)
|
||||||
|
|
||||||
|
set_form_param(page, form_name)
|
||||||
|
output = submit_and_download(page, context, form_name, country, study_label)
|
||||||
|
results.append(output)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
print(f"\nHotovo! Staženo {len(results)} formulářů. Prohlížeč zavřen.")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
country_arg = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
|
download_datalisting(
|
||||||
|
study="77242113UCO3001",
|
||||||
|
forms=["Trial Disposition (Completion / Discontinuation)"],
|
||||||
|
country=country_arg,
|
||||||
|
)
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
|
import_to_mongo.py
|
||||||
|
Verze: 1.0
|
||||||
|
Datum: 2026-05-27
|
||||||
|
|
||||||
Import EDC Data Listing CSV do MongoDB (databáze: edc).
|
Import EDC Data Listing CSV do MongoDB (databáze: edc).
|
||||||
|
|
||||||
Kolekce: {STUDY}.{FormName} (např. UCO3001.ConcomitantTherapy)
|
Kolekce: {STUDY}.{FormName} (např. UCO3001.ConcomitantTherapy)
|
||||||
Filtr: pouze řádky s SiteGroupName == "CZE"
|
Filtr: pouze řádky s SiteGroupName == "CZE"
|
||||||
Historie: při změně fields se stará verze uloží do pole history[]
|
Historie: při změně fields se stará verze uloží do pole history[]
|
||||||
|
Po importu přesune zpracované CSV do downloads/Zpracovano/
|
||||||
|
|
||||||
Použití:
|
Použití:
|
||||||
python import_to_mongo.py # importuje všechny CSV z downloads/
|
python import_to_mongo.py # importuje všechny CSV z downloads/
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from download_edc_datalistings import download_datalisting
|
||||||
|
|
||||||
|
download_datalisting(
|
||||||
|
study="77242113UCO3001",
|
||||||
|
forms=[
|
||||||
|
"Trial Disposition (Completion / Discontinuation)",
|
||||||
|
"Date of Visit",
|
||||||
|
"Concomitant Therapy",
|
||||||
|
],
|
||||||
|
country="CZE",
|
||||||
|
)
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from download_uco3001 import download_datalisting_reports_3001
|
|
||||||
|
|
||||||
# === Vyber jeden řádek, odkomentuj ho a spusť ===
|
|
||||||
|
|
||||||
# --- Trial Disposition ---
|
|
||||||
# download_datalisting_reports_3001("Trial Disposition (Completion / Discontinuation)")
|
|
||||||
# download_datalisting_reports_3001("Date of Visit")
|
|
||||||
download_datalisting_reports_3001("Concomitant Therapy", country="CZE")
|
|
||||||
# download_datalisting_reports_3001("Trial Disposition (Completion / Discontinuation)", country="CZE")
|
|
||||||
|
|
||||||
# --- Date of Visit ---
|
|
||||||
# download_datalisting_reports_3001("Date of Visit")
|
|
||||||
# download_datalisting_reports_3001("Date of Visit", country="CZE")
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MCP server pro MongoDB — používá FastMCP.
|
||||||
|
Spustit: python mcp_mongo.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, date
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
MONGO_HOST = "192.168.1.76"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
print(msg, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MongoClient(MONGO_HOST, serverSelectionTimeoutMS=5000)
|
||||||
|
client.server_info()
|
||||||
|
log(f"Connected to MongoDB ({MONGO_HOST})")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"MongoDB connection failed: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(obj):
|
||||||
|
"""Make MongoDB documents JSON-serializable."""
|
||||||
|
if isinstance(obj, ObjectId):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
if isinstance(obj, date):
|
||||||
|
return obj.isoformat()
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return obj.decode("utf-8", errors="replace")
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {k: serialize(v) for k, v in obj.items()}
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [serialize(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def parse_filter(filter_json: Optional[str]) -> dict:
|
||||||
|
"""Parse JSON string to dict. Returns {} on None/empty."""
|
||||||
|
if not filter_json or not filter_json.strip():
|
||||||
|
return {}
|
||||||
|
return json.loads(filter_json)
|
||||||
|
|
||||||
|
|
||||||
|
mcp = FastMCP("janssen-mongo")
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_databases() -> dict:
|
||||||
|
"""List all databases on the MongoDB server (excludes admin/config/local)."""
|
||||||
|
try:
|
||||||
|
skip = {"admin", "config", "local"}
|
||||||
|
dbs = [d["name"] for d in client.list_databases() if d["name"] not in skip]
|
||||||
|
return {"count": len(dbs), "databases": dbs}
|
||||||
|
except Exception:
|
||||||
|
log(f"list_databases error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_collections(db: str) -> dict:
|
||||||
|
"""List all collections in a database."""
|
||||||
|
try:
|
||||||
|
cols = sorted(client[db].list_collection_names())
|
||||||
|
return {"db": db, "count": len(cols), "collections": cols}
|
||||||
|
except Exception:
|
||||||
|
log(f"list_collections error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def collection_stats(db: str, collection: str) -> dict:
|
||||||
|
"""Returns document count + schema sample (fields and types from first document)."""
|
||||||
|
try:
|
||||||
|
col = client[db][collection]
|
||||||
|
count = col.estimated_document_count()
|
||||||
|
sample = col.find_one()
|
||||||
|
schema = {}
|
||||||
|
if sample:
|
||||||
|
for k, v in sample.items():
|
||||||
|
schema[k] = type(v).__name__
|
||||||
|
return {"db": db, "collection": collection, "count": count, "schema": schema}
|
||||||
|
except Exception:
|
||||||
|
log(f"collection_stats error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def find_documents(
|
||||||
|
db: str,
|
||||||
|
collection: str,
|
||||||
|
filter_json: Optional[str] = None,
|
||||||
|
projection_json: Optional[str] = None,
|
||||||
|
sort_json: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> dict:
|
||||||
|
"""Query documents. filter/projection/sort are JSON strings, e.g. '{"patient_code":"CZ1-01"}'.
|
||||||
|
sort example: '{"last_seen_at": -1}'. Limit max 500.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
col = client[db][collection]
|
||||||
|
filt = parse_filter(filter_json)
|
||||||
|
proj = parse_filter(projection_json) or None
|
||||||
|
sort_spec = parse_filter(sort_json)
|
||||||
|
limit = min(limit, 500)
|
||||||
|
|
||||||
|
cursor = col.find(filt, proj)
|
||||||
|
if sort_spec:
|
||||||
|
cursor = cursor.sort(list(sort_spec.items()))
|
||||||
|
cursor = cursor.limit(limit)
|
||||||
|
|
||||||
|
docs = [serialize(doc) for doc in cursor]
|
||||||
|
return {"count": len(docs), "docs": docs}
|
||||||
|
except Exception:
|
||||||
|
log(f"find_documents error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def aggregate(db: str, collection: str, pipeline_json: str) -> dict:
|
||||||
|
"""Run a MongoDB aggregation pipeline. pipeline_json is a JSON array of stages,
|
||||||
|
e.g. '[{"$group": {"_id": "$patient_code", "count": {"$sum": 1}}}]'.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
col = client[db][collection]
|
||||||
|
pipeline = json.loads(pipeline_json)
|
||||||
|
results = [serialize(doc) for doc in col.aggregate(pipeline)]
|
||||||
|
return {"count": len(results), "results": results}
|
||||||
|
except Exception:
|
||||||
|
log(f"aggregate error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def distinct_values(db: str, collection: str, field: str, filter_json: Optional[str] = None) -> dict:
|
||||||
|
"""Return distinct values of a field, optionally filtered."""
|
||||||
|
try:
|
||||||
|
col = client[db][collection]
|
||||||
|
filt = parse_filter(filter_json)
|
||||||
|
values = [serialize(v) for v in col.distinct(field, filt)]
|
||||||
|
return {"field": field, "count": len(values), "values": values}
|
||||||
|
except Exception:
|
||||||
|
log(f"distinct_values error: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
log("MCP MongoDB server started (FastMCP)")
|
||||||
|
mcp.run()
|
||||||