Skip to content

Commit 449f5a1

Browse files
author
“Carmel
committed
Send progress results to ldot
1 parent 15412e1 commit 449f5a1

8 files changed

Lines changed: 207 additions & 48 deletions

app.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from new_ldot_workflows.b_2_get_qualtrics_links import add_individuals_to_survey
99
from new_ldot_workflows.b_2_send_links_to_ldot import send_links_to_ldot
1010
from new_ldot_workflows.b_3_get_incomplete_subjects import get_incomplete_subjects
11-
# from new_ldot_workflows.b_4_get_individual_progress import get_individual_progress
11+
from new_ldot_workflows.b_4_get_individual_progress import get_individual_progress
1212

1313
app = Flask(__name__)
1414

@@ -41,9 +41,16 @@ def button1():
4141

4242
ldot_study_id = ldot_vars.get("ldot_study_id")
4343
link_creation_eaid = ldot_vars.get("eaid_qualtrics_survey_link_creation_to_do_date")
44-
link_completed_eaid = ldot_vars.get("eaid_qualtrics_survey_link_completed")
44+
link_completed_eaid = ldot_vars.get("eaid_qualtrics_survey_link_creation_completed")
4545

46-
new_subjects_ids = get_new_subjects(ldot_study_id, link_creation_eaid, link_completed_eaid)
46+
try:
47+
new_subjects_ids = get_new_subjects(ldot_study_id, link_creation_eaid, link_completed_eaid)
48+
except requests.RequestException as e:
49+
return jsonify({"success": False, "message": f"Failed to fetch new subjects from LDOT: {e}"}), 502
50+
except (KeyError, ValueError, TypeError) as e:
51+
return jsonify({"success": False, "message": f"Unexpected LDOT response while fetching new subjects: {e}"}), 502
52+
except Exception as e:
53+
return jsonify({"success": False, "message": f"Unexpected error in button1: {e}"}), 500
4754

4855
message = f"Found {len(new_subjects_ids)} new subjects in this study"
4956
return jsonify({"success": True, "message": message, "new_subject_ids": new_subjects_ids})
@@ -61,7 +68,10 @@ def button2():
6168
return jsonify({"success": False, "message": "Missing study_id in request"}), 400
6269

6370
if not new_subject_ids:
64-
return jsonify({"success": False, "message": "Missing new_subject_ids in request"}), 400
71+
return jsonify({
72+
"success": True,
73+
"message": f"No new subjects to process",
74+
})
6575

6676
# Lookup study variables from STUDIES config
6777
study, ldot_vars, qualtrics_vars = get_study_settings(study_id)
@@ -78,9 +88,6 @@ def button2():
7888
directory_id = qualtrics_vars.get("directory_id")
7989
distribution_id = qualtrics_vars.get("distribution_id")
8090

81-
if not ldot_study_id:
82-
return jsonify({"success": False, "message": f"Missing ldot_study_id for study_id: {study_id}"}), 400
83-
8491
debug_inputs = {
8592
"study_id": study_id,
8693
"ldot_study_id": ldot_study_id,
@@ -120,9 +127,10 @@ def button3():
120127
eaid_survey_progress_completed = ldot_vars.get("eaid_survey_progress_completed")
121128

122129
try:
123-
result = f"Button 3 executed for study {study_id}"
124130
subjects_not_completed_survey = get_incomplete_subjects(ldot_study_id, eaid_survey_invitation_completed, eaid_survey_progress_completed)
125-
return jsonify({"success": True, "message": result, "subject_ids": subjects_not_completed_survey})
131+
print("Subjects not completed survey:", subjects_not_completed_survey)
132+
message = f"Found {len(subjects_not_completed_survey)} subjects who have not completed the survey"
133+
return jsonify({"success": True, "message": message, "subject_ids": subjects_not_completed_survey})
126134
except Exception as e:
127135
return jsonify({"success": False, "message": str(e)})
128136

@@ -137,17 +145,15 @@ def button4():
137145
if not study_id:
138146
return jsonify({"success": False, "message": "Missing study_id in request"}), 400
139147

140-
# if not subject_ids:
141-
# return jsonify({"success": False, "message": "Missing subject_ids in request"}), 400
142-
143-
# study, vars = get_study_settings(study_id)
144-
# if not study:
145-
# return jsonify({"success": False, "message": f"Unknown study_id: {study_id}"}), 400
148+
study, ldot_vars, qualtrics_vars = get_study_settings(study_id)
146149

147-
# qualtrics_survey_id = vars.get("qualtrics_survey_id")
148-
# embedded_data_field = vars.get("embedded_data_field")
150+
ldot_study_id = ldot_vars.get("ldot_study_id")
151+
id_deelnemer_entity = ldot_vars.get("id_deelnemer_entity")
152+
id_location = ldot_vars.get("id_location")
153+
qualtrics_survey_id = qualtrics_vars.get("qualtrics_survey_id")
154+
embedded_data_field = qualtrics_vars.get("embedded_data_field")
149155

150-
# participant_to_progress_dict = get_individual_progress(subject_ids, embedded_data_field, qualtrics_survey_id)
156+
participant_to_progress_dict = get_individual_progress(ldot_study_id, id_deelnemer_entity, id_location, subject_ids, embedded_data_field, qualtrics_survey_id)
151157

152158

153159
# return jsonify({
Binary file not shown.
Binary file not shown.
Binary file not shown.

new_ldot_workflows/b_1_get_new_subjects.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@
77
CLIENT_SECRET = config["client_secret"]
88
LDOT_API_URL = config["LDOT_API_URL"]
99

10+
11+
def _response_snippet(response: requests.Response, limit: int = 300) -> str:
12+
text = response.text.strip().replace("\n", " ")
13+
return text[:limit]
14+
15+
16+
def _format_response_error(prefix: str, response: requests.Response) -> str:
17+
return f"{prefix} (status {response.status_code}): {_response_snippet(response)}"
18+
1019
def get_new_subjects(study_id: str, eaid_qualtrics_survey_link_creation_to_do_date: str, eaid_qualtrics_survey_link_completed: str) -> list:
1120
"""Get subjects that have not yet been added to Qualtrics by checking their event actions"""
1221

22+
print(f"Inputs into get_new_subjects: study_id={study_id}, eaid_qualtrics_survey_link_creation_to_do_date={eaid_qualtrics_survey_link_creation_to_do_date}, eaid_qualtrics_survey_link_completed={eaid_qualtrics_survey_link_completed}")
23+
1324
response = requests.post(
1425
"https://accware.memic.maastrichtuniversity.nl/ldot_identity_server/connect/token",
1526
data={
@@ -19,7 +30,16 @@ def get_new_subjects(study_id: str, eaid_qualtrics_survey_link_creation_to_do_da
1930
}
2031
)
2132

22-
token = response.json()["access_token"]
33+
response.raise_for_status()
34+
35+
try:
36+
token_payload = response.json()
37+
except ValueError as e:
38+
raise ValueError(_format_response_error("LDOT token endpoint returned invalid JSON", response)) from e
39+
40+
token = token_payload.get("access_token")
41+
if not token:
42+
raise ValueError(_format_response_error("LDOT token endpoint did not include an access_token", response))
2343

2444
headers={"accept": "application/json",
2545
"Authorization": f"Bearer {token}"
@@ -32,7 +52,11 @@ def get_new_subjects(study_id: str, eaid_qualtrics_survey_link_creation_to_do_da
3252
)
3353

3454
response.raise_for_status()
35-
payload = response.json()
55+
56+
try:
57+
payload = response.json()
58+
except ValueError as e:
59+
raise ValueError(_format_response_error("LDOT creation-to-do lookup returned invalid JSON", response)) from e
3660
study_event_actions = payload.get("Data", {}).get("StudyEventActions", [])
3761

3862
subjects_link_creation_to_do = set([
@@ -41,29 +65,33 @@ def get_new_subjects(study_id: str, eaid_qualtrics_survey_link_creation_to_do_da
4165
if action.get("SubjectGuid")
4266
])
4367

44-
# print(f"Subjects with link creation to do: {subjects_link_creation_to_do}")
68+
print(f"Subjects with link creation to do: {subjects_link_creation_to_do}")
4569

4670
# Get subjects that have link completed date
4771
response = requests.get(
4872
f"https://accware.memic.maastrichtuniversity.nl/memic_ldot_api/api/v1.1/{study_id}/Action/{eaid_qualtrics_survey_link_completed}",
4973
headers=headers
5074
)
5175
response.raise_for_status()
52-
payload = response.json()
76+
77+
try:
78+
payload = response.json()
79+
except ValueError as e:
80+
raise ValueError(_format_response_error("LDOT completed lookup returned invalid JSON", response)) from e
5381
study_event_actions_completed = payload.get("Data", {}).get("StudyEventActions", [])
5482
subjects_link_completed = set([
5583
action["SubjectGuid"]
5684
for action in study_event_actions_completed
5785
if action.get("SubjectGuid")
5886
])
5987

60-
# print(f"Subjects with link completed: {subjects_link_completed}")
88+
print(f"Subjects with link completed: {subjects_link_completed}")
6189

6290
# Filter out subjects that have already completed the link
6391
new_subjects = subjects_link_creation_to_do - subjects_link_completed
6492

65-
# print(f"New subjects: {new_subjects}")
66-
return list(new_subjects)
93+
print(f"New subjects: {new_subjects}")
94+
return list(new_subjects) if new_subjects else []
6795

6896

6997

new_ldot_workflows/b_3_get_incomplete_subjects.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
def get_incomplete_subjects(study_id: str, eaid_survey_invitation_completed: str = None, eaid_survey_progress_completed: str = None) -> list:
1212
"""Get subjects that have not yet completed the survey by checking their event actions"""
1313

14+
print("Inputs to get_incomplete_subjects: study_id={}, eaid_survey_invitation_completed={}, eaid_survey_progress_completed={}".format(study_id, eaid_survey_invitation_completed, eaid_survey_progress_completed))
15+
1416
response = requests.post(
1517
"https://accware.memic.maastrichtuniversity.nl/ldot_identity_server/connect/token",
1618
data={
@@ -35,8 +37,9 @@ def get_incomplete_subjects(study_id: str, eaid_survey_invitation_completed: str
3537

3638
subjects_with_survey_invitation_completed = set()
3739

38-
for action in result.json().get("Data", {}).get("StudyEventActions", []):
39-
subjects_with_survey_invitation_completed.add(action["SubjectGuid"])
40+
# print(result.json())
41+
# for action in result.json().get("Data", {}).get("StudyEventActions", []):
42+
# subjects_with_survey_invitation_completed.add(action["SubjectGuid"])
4043

4144
# print(f"Subjects with survey invitation completed: {subjects_with_survey_invitation_completed}")
4245

@@ -45,15 +48,20 @@ def get_incomplete_subjects(study_id: str, eaid_survey_invitation_completed: str
4548
headers=headers
4649
)
4750
result.raise_for_status()
51+
print(result.json())
4852

49-
subjects_with_survey_progress_completed = set()
50-
for action in result.json().get("Data", {}).get("StudyEventActions", []):
51-
subjects_with_survey_progress_completed.add(action["SubjectGuid"])
53+
# subjects_with_survey_progress_completed = set()
54+
# for action in result.json().get("Data", {}).get("StudyEventActions", []):
55+
# subjects_with_survey_progress_completed.add(action["SubjectGuid"])
5256

5357
# print(f"Subjects with survey progress completed: {subjects_with_survey_progress_completed}")
5458

55-
incomplete_subjects = subjects_with_survey_invitation_completed - subjects_with_survey_progress_completed
56-
return list(incomplete_subjects)
59+
# incomplete_subjects = subjects_with_survey_invitation_completed - subjects_with_survey_progress_completed
60+
# return list(incomplete_subjects)
5761

5862
if __name__ == "__main__":
59-
print(get_incomplete_subjects("5c9c6a47-c8d7-8142-a8c8-ccdcb8a8044b", "9f09f2cf-32cc-844d-9a41-7a44accd3dfd", '120d298d-9aa9-084b-abc6-432148db0e10'))
63+
64+
ldot_study_id = "5c9c6a47-c8d7-8142-a8c8-ccdcb8a8044b"
65+
eaid_survey_invitation_completed = "337ecba2-3dd3-1141-8c38-6cffdcfbd7eb"
66+
eaid_survey_progress_completed = "120d298d-9aa9-084b-abc6-432148db0e10"
67+
print(get_incomplete_subjects(ldot_study_id, eaid_survey_invitation_completed, eaid_survey_progress_completed))

new_ldot_workflows/b_4_get_individual_progress.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import requests
2-
from qualtrics_settings import BASE_URL, HEADERS, QualtricsAPIError
32
import time
43
import zipfile
54
import io
65
import pandas as pd
6+
import json
7+
8+
with open("new_ldot_workflows/qualtrics_config.json") as f:
9+
config = json.load(f)
10+
QUALTRICS_BASE_URL = config["QUALTRICS_BASE_URL"]
11+
HEADERS = config["HEADERS"]
12+
13+
with open("new_ldot_workflows/ldot_config.json") as f:
14+
config = json.load(f)
15+
CLIENT_ID = config["client_id"]
16+
CLIENT_SECRET = config["client_secret"]
17+
LDOT_API_URL = config["LDOT_API_URL"]
18+
19+
class QualtricsAPIError(Exception):
20+
"""Custom exception for Qualtrics API errors"""
21+
pass
722

823
def create_data_export(survey_id: str, incomplete_bool: bool = True) -> str:
924
"""Create a data export request for a given survey.
@@ -17,7 +32,7 @@ def create_data_export(survey_id: str, incomplete_bool: bool = True) -> str:
1732
str: The ID of the data export request.
1833
"""
1934
print("Creating data export request... ")
20-
endpoint = f"{BASE_URL}/surveys/{survey_id}/export-responses"
35+
endpoint = f"{QUALTRICS_BASE_URL}/surveys/{survey_id}/export-responses"
2136
payload = {
2237
"format": "csv",
2338
"exportResponsesInProgress": incomplete_bool
@@ -51,7 +66,7 @@ def check_export_progress(request_id: str, survey_id: str, returns: str) -> str|
5166
str: The percent complete if the request is still processing, or the file ID if complete.
5267
"""
5368
print("Checking on export progress... ")
54-
endpoint = f"{BASE_URL}/surveys/{survey_id}/export-responses/{request_id}"
69+
endpoint = f"{QUALTRICS_BASE_URL}/surveys/{survey_id}/export-responses/{request_id}"
5570
try:
5671
result = requests.get(endpoint, headers=HEADERS)
5772
result.raise_for_status()
@@ -84,7 +99,7 @@ def download_export(file_id: str, survey_id: str) -> pd.DataFrame:
8499
pd.DataFrame: The survey response data as a DataFrame.
85100
"""
86101
print("Downloading export file... ")
87-
endpoint = f"{BASE_URL}/surveys/{survey_id}/export-responses/{file_id}/file"
102+
endpoint = f"{QUALTRICS_BASE_URL}/surveys/{survey_id}/export-responses/{file_id}/file"
88103
try:
89104
download = requests.get(
90105
endpoint,
@@ -133,6 +148,7 @@ def check_inputs_validity(participant_study_id: str, embedded_data_field: str, s
133148
raise ValueError("Invalid participant_study_id. It should be a non-empty string.")
134149
return True
135150

151+
136152
def get_responses_as_df(survey_id: str, incomplete_bool: bool) -> pd.DataFrame:
137153
"""Wrapper function to request export, wait for it, download it, and convert to DataFrame.
138154
@@ -157,13 +173,49 @@ def get_responses_as_df(survey_id: str, incomplete_bool: bool) -> pd.DataFrame:
157173
df = download_export(file_id, survey_id)
158174
return df
159175

160-
def get_individual_progress(participant_study_ids: list, embedded_data_field: str, survey_id: str) -> float:
176+
177+
def subject_id_to_study_identifier(ldot_study_id: str, id_deelnemer_entity: str, id_location: str, subject_ids: list) -> str:
178+
"""Post the Qualtrics links back to Ldot for new subjects"""
179+
180+
response = requests.post(
181+
"https://accware.memic.maastrichtuniversity.nl/ldot_identity_server/connect/token",
182+
data={
183+
"grant_type": "client_credentials",
184+
"client_id": CLIENT_ID,
185+
"client_secret": CLIENT_SECRET
186+
}
187+
)
188+
token = response.json()["access_token"]
189+
headers={"accept": "application/json",
190+
"Authorization": f"Bearer {token}"
191+
}
192+
193+
subject_id_to_study_identifier_dict = {}
194+
for subject_id in subject_ids:
195+
# Populate the Qualtrics link in Ldot for the subject
196+
response = requests.post(
197+
f"https://accware.memic.maastrichtuniversity.nl/memic_ldot_api/api/v1.1/{ldot_study_id}/Subject/",
198+
headers=headers,
199+
json = {
200+
"subjectGuid": subject_id,
201+
"entityId": id_deelnemer_entity,
202+
"locationSubjectId": id_location,
203+
"doOverwriteNulls": False,
204+
}
205+
)
206+
subject_id_to_study_identifier_dict[subject_id] = response.json().get("Data", {}).get("Subject").get("RegistrationId")
207+
208+
209+
return subject_id_to_study_identifier_dict
210+
211+
212+
def get_individual_progress(ldot_study_id: str, id_deelnemer_entity: str, id_location: str, subject_ids: list, embedded_data_field: str, survey_id: str) -> float:
161213
"""Wrapper function that gets the individual progress of a participant in a survey."""
162214
# Check for invalid inputs
215+
check_inputs_validity(subject_ids[0], embedded_data_field, survey_id) # TODO: Check this
163216

164217
participant_to_progress_dict = {}
165-
166-
check_inputs_validity(participant_study_ids[0], embedded_data_field, survey_id)
218+
subject_id_to_study_identifier_dict = subject_id_to_study_identifier(ldot_study_id, id_deelnemer_entity, id_location, subject_ids)
167219

168220
completed_responses_df = get_responses_as_df(survey_id, incomplete_bool=False)
169221
incompleted_responses_df = get_responses_as_df(survey_id, incomplete_bool=True)
@@ -172,21 +224,31 @@ def get_individual_progress(participant_study_ids: list, embedded_data_field: st
172224
if not embedded_data_field in df.columns:
173225
raise QualtricsAPIError(f"Embedded data field '{embedded_data_field}' not found in survey data. Please check the field name.")
174226

175-
for participant_study_id in participant_study_ids:
176-
individual_progress = df[df[embedded_data_field] == participant_study_id]
227+
for subject_id, study_identifier in subject_id_to_study_identifier_dict.items():
228+
individual_progress = df[df[embedded_data_field] == study_identifier]
229+
print("This is the individual progress for subject_id {}, study_identifier {}: {}".format(subject_id, study_identifier, individual_progress))
177230
if not individual_progress.empty:
178231
individual_progress = individual_progress["Progress"].values[0]
179-
participant_to_progress_dict[participant_study_id] = individual_progress
232+
participant_to_progress_dict[subject_id] = individual_progress
180233
else:
181-
participant_to_progress_dict[participant_study_id] = None # or some indicator that the participant ID was not found in the data
234+
participant_to_progress_dict[subject_id] = 0 # Indicator that the subject ID was not found in the data
182235

183236
return participant_to_progress_dict
184237

185238
if __name__ == "__main__":
186-
# Example usage
187-
participant_study_ids=["11111TEST11111", "22222TEST22222", "33333TEST33333"]
239+
# # Example usage
188240
survey_id="SV_efCMOg6wHU0T8ii"
189241
embedded_data_field = "study_id_child"
190242

191-
participant_to_progress_dict = get_individual_progress(participant_study_ids, embedded_data_field, survey_id)
192-
print(participant_to_progress_dict)
243+
244+
ldot_study_id = "5c9c6a47-c8d7-8142-a8c8-ccdcb8a8044b"
245+
subject_ids = ["352fb9d8-962f-4735-9fc7-7b4e18109a51"]
246+
id_deelnemer_entity = "7f61b810-00ed-1d41-8a33-4164f25ebad0"
247+
id_location = "427f304f-9d95-44f5-8f7b-d6a1ce1db293"
248+
embedded_data_field = "study_id_child"
249+
250+
participant_to_progress_dict = get_individual_progress(ldot_study_id, id_deelnemer_entity, id_location, subject_ids, embedded_data_field, survey_id)
251+
print(participant_to_progress_dict)
252+
253+
254+
# subject_id_to_study_identifier_dict = subject_id_to_study_identifier(ldot_study_id, id_deelnemer_entity, id_location, subject_ids)

0 commit comments

Comments
 (0)