-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
342 lines (282 loc) · 13.2 KB
/
Copy pathapp.py
File metadata and controls
342 lines (282 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
from threading import Lock
from dotenv import load_dotenv, dotenv_values
from flask import Flask, render_template, request, jsonify
import requests
import yaml
import os
from new_ldot_workflows.ldot_client import LdotClient
from new_ldot_workflows.qualtrics_client import QualtricsClient
from new_ldot_workflows.b_1_get_new_subjects import get_new_subjects
from new_ldot_workflows.b_2_get_qualtrics_links import add_individuals_to_survey
from new_ldot_workflows.b_2_send_links_to_ldot import send_links_to_ldot
from new_ldot_workflows.b_3_get_incomplete_subjects import get_incomplete_subjects
from new_ldot_workflows.b_4_get_individual_progress import get_individual_progress
from new_ldot_workflows.b_4_send_progress_to_ldot import send_progress_to_ldot
from new_ldot_workflows.logging_utils import QualtricsAPIError
app = Flask(__name__)
CONFIG_PATH = Path(__file__).resolve().with_name("study_configs.yaml")
STUDIES = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8"))
LDOT_API_URL="https://accware.memic.maastrichtuniversity.nl/memic_ldot_api/api/v1.1"
LDOT_TOKEN_URL="https://accware.memic.maastrichtuniversity.nl/ldot_identity_server/connect/token"
QUALTRICS_BASE_URL="https://fra1.qualtrics.com/API/v3"
CLIENT_CACHE = {}
CLIENT_CACHE_LOCK = Lock()
load_dotenv()
@dataclass
class StudySettings:
study_id: str
name: str
config_path: Optional[str] = None
ldot_study_id: Optional[str] = None
id_deelnemer_entity: Optional[str] = None
id_location: Optional[str] = None
custom_var_qualtrics_link: Optional[str] = None
eaid_qualtrics_survey_link_creation_to_do_date: Optional[str] = None
eaid_qualtrics_survey_link_creation_completed: Optional[str] = None
eaid_survey_invitation_completed: Optional[str] = None
eaid_survey_progress_completed: Optional[str] = None
survey_id: Optional[str] = None
mailing_list_id: Optional[str] = None
embedded_data_field: Optional[str] = None
directory_id: Optional[str] = None
distribution_id: Optional[str] = None
def get_study_settings(study_key: str):
study_variables = STUDIES.get(study_key)
if not study_variables:
return None
ldot_vars = study_variables.get("ldot_variables", {})
qualtrics_vars = study_variables.get("qualtrics_variables", {})
return StudySettings(
study_id=study_key,
name=study_variables.get("name", study_key),
config_path=study_variables.get("config_path"),
ldot_study_id=ldot_vars.get("ldot_study_id"),
id_deelnemer_entity=ldot_vars.get("id_deelnemer_entity"),
id_location=ldot_vars.get("id_location"),
custom_var_qualtrics_link=ldot_vars.get("custom_var_qualtrics_link"),
eaid_qualtrics_survey_link_creation_to_do_date=ldot_vars.get("eaid_qualtrics_survey_link_creation_to_do_date"),
eaid_qualtrics_survey_link_creation_completed=ldot_vars.get("eaid_qualtrics_survey_link_creation_completed"),
eaid_survey_invitation_completed=ldot_vars.get("eaid_survey_invitation_completed"),
eaid_survey_progress_completed=ldot_vars.get("eaid_survey_progress_completed"),
survey_id=qualtrics_vars.get("qualtrics_survey_id"),
mailing_list_id=qualtrics_vars.get("mailing_list_id"),
embedded_data_field=qualtrics_vars.get("embedded_data_field"),
directory_id=qualtrics_vars.get("directory_id"),
distribution_id=qualtrics_vars.get("distribution_id"),
)
def get_clients_for_study(study_variables: StudySettings):
if not study_variables.config_path:
raise ValueError(f"Missing config_path for study_id: {study_variables.study_id}")
cached_clients = CLIENT_CACHE.get(study_variables.study_id)
if cached_clients:
return cached_clients
with CLIENT_CACHE_LOCK:
cached_clients = CLIENT_CACHE.get(study_variables.study_id)
if cached_clients:
return cached_clients
study_env_path = (CONFIG_PATH.parent / "app-secrets" / study_variables.config_path).resolve()
if not study_env_path.exists():
raise FileNotFoundError(f"Study config file not found: {study_env_path}")
study_env = dotenv_values(study_env_path)
ldot_client_id = study_env.get("LDOT_client_id") or os.environ.get("LDOT_client_id")
ldot_client_secret = study_env.get("LDOT_client_secret") or os.environ.get("LDOT_client_secret")
qualtrics_api_token = study_env.get("QUALTRICS_API_TOKEN") or os.environ.get("QUALTRICS_API_TOKEN")
missing_secrets = [
secret_name
for secret_name, secret_value in (
("LDOT_client_id", ldot_client_id),
("LDOT_client_secret", ldot_client_secret),
("QUALTRICS_API_TOKEN", qualtrics_api_token),
)
if not secret_value
]
if missing_secrets:
raise KeyError(f"Missing secrets in {study_env_path}: {', '.join(missing_secrets)}")
ldot_client = LdotClient(
token_url=LDOT_TOKEN_URL,
api_url=LDOT_API_URL,
client_id=ldot_client_id,
client_secret=ldot_client_secret,
)
qualtrics_client = QualtricsClient(
api_url=QUALTRICS_BASE_URL,
token=qualtrics_api_token,
)
CLIENT_CACHE[study_variables.study_id] = (ldot_client, qualtrics_client)
return CLIENT_CACHE[study_variables.study_id]
@app.route("/")
def index():
return render_template("index.html", studies=STUDIES)
@app.route("/api/button1", methods=["POST"])
def button1():
"""Get the participants who have not yet been given a survey link and return their subjectIDs"""
data = request.json
study_id = data.get("study_id")
study_variables = get_study_settings(study_id)
if not study_variables:
return jsonify({"success": False, "message": f"Unknown study_id: {study_id}"}), 400
try:
ldot_client, _ = get_clients_for_study(study_variables)
except (KeyError, ValueError) as e:
return jsonify({"success": False, "message": str(e)}), 500
try:
new_subjects_ids = get_new_subjects(
ldot_client,
study_variables.ldot_study_id,
study_variables.eaid_qualtrics_survey_link_creation_to_do_date,
study_variables.eaid_qualtrics_survey_link_creation_completed,
)
except QualtricsAPIError as e:
return jsonify({"success": False, "message": str(e)}), 502
except requests.RequestException as e:
return jsonify({"success": False, "message": f"Failed to fetch new subjects from Ldot: {e}"}), 502
except (KeyError, ValueError, TypeError) as e:
return jsonify({"success": False, "message": f"Unexpected Ldot response while fetching new subjects: {e}"}), 502
except Exception as e:
return jsonify({"success": False, "message": f"Unexpected error in button1: {e}"}), 500
message = f"Found {len(new_subjects_ids)} new subjects in this study"
return jsonify({"success": True, "message": message, "new_subject_ids": new_subjects_ids})
@app.route("/api/button2", methods=["POST"])
def button2():
"""Uses subjectIDs from button 1 to add them to Qualtrics and send the links back to Ldot"""
data = request.json
study_id = data.get("study_id")
new_subject_ids = data.get("new_subject_ids") or data.get("subject_ids")
if not study_id:
return jsonify({"success": False, "message": "Missing study_id in request"}), 400
if not new_subject_ids:
return jsonify({
"success": True,
"message": f"No new subjects to process",
})
# Lookup study variables from STUDIES config
study_variables = get_study_settings(study_id)
if not study_variables:
return jsonify({"success": False, "message": f"Unknown study_id: {study_id}"}), 400
debug_inputs = {
"study_id": study_id,
"ldot_study_id": study_variables.ldot_study_id,
"id_deelnemer_entity": study_variables.id_deelnemer_entity,
"id_location": study_variables.id_location,
"new_subject_ids": new_subject_ids,
"qualtrics_survey_id": study_variables.survey_id,
"mailing_list_id": study_variables.mailing_list_id,
"embedded_data_field": study_variables.embedded_data_field,
"directory_id": study_variables.directory_id,
"distribution_id": study_variables.distribution_id,
}
try:
ldot_client, qualtrics_client = get_clients_for_study(study_variables)
except (KeyError, ValueError) as e:
return jsonify({"success": False, "message": str(e), "debug_inputs": debug_inputs}), 500
try:
subject_id_to_link_dict = add_individuals_to_survey(
ldot_client,
qualtrics_client,
new_subject_ids,
study_variables.ldot_study_id,
study_variables.id_deelnemer_entity,
study_variables.embedded_data_field,
study_variables.distribution_id,
study_variables.survey_id,
study_variables.mailing_list_id,
study_variables.directory_id,
)
send_links_to_ldot(
ldot_client,
study_variables.ldot_study_id,
study_variables.id_deelnemer_entity,
study_variables.id_location,
study_variables.custom_var_qualtrics_link,
study_variables.eaid_qualtrics_survey_link_creation_completed,
subject_id_to_link_dict,
)
except QualtricsAPIError as e:
return jsonify({"success": False, "message": str(e), "debug_inputs": debug_inputs}), 502
except Exception as e:
return jsonify({"success": False, "message": f"Unexpected error in button2: {e}", "debug_inputs": debug_inputs}), 500
return jsonify({
"success": True,
"message": f"Processed {len(new_subject_ids)} subject IDs",
"debug_inputs": debug_inputs,
"subject_id_to_link_dict": subject_id_to_link_dict
})
@app.route("/api/button3", methods=["POST"])
def button3():
"""Third API call - returns list of subjectIDs"""
data = request.json
study_id = data.get("study_id")
study_variables = get_study_settings(study_id)
if not study_variables:
return jsonify({"success": False, "message": f"Unknown study_id: {study_id}"}), 400
try:
ldot_client, _ = get_clients_for_study(study_variables)
except (KeyError, ValueError) as e:
return jsonify({"success": False, "message": str(e)}), 500
try:
subjects_not_completed_survey = get_incomplete_subjects(
ldot_client,
study_variables.ldot_study_id,
study_variables.eaid_survey_invitation_completed,
study_variables.eaid_survey_progress_completed,
)
message = f"Found {len(subjects_not_completed_survey)} subjects who have not completed the survey"
return jsonify({"success": True, "message": message, "subject_ids": subjects_not_completed_survey})
except QualtricsAPIError as e:
return jsonify({"success": False, "message": str(e)}), 502
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@app.route("/api/button4", methods=["POST"])
def button4():
"""Fourth API call - uses subjectIDs from button3"""
data = request.json
study_id = data.get("study_id")
subject_ids = data.get("subject_ids") # List of subjectIDs from button3
if not study_id:
return jsonify({"success": False, "message": "Missing study_id in request"}), 400
study_variables = get_study_settings(study_id)
if not study_variables:
return jsonify({"success": False, "message": f"Unknown study_id: {study_id}"}), 400
try:
ldot_client, qualtrics_client = get_clients_for_study(study_variables)
except (KeyError, ValueError) as e:
return jsonify({"success": False, "message": str(e)}), 500
try:
participant_to_progress_dict = get_individual_progress(
ldot_client,
qualtrics_client,
study_variables.ldot_study_id,
study_variables.id_deelnemer_entity,
study_variables.id_location,
subject_ids,
study_variables.embedded_data_field,
study_variables.survey_id,
)
send_progress_to_ldot(
ldot_client,
study_variables.ldot_study_id,
study_variables.eaid_survey_progress_completed,
participant_to_progress_dict,
)
except QualtricsAPIError as e:
return jsonify({"success": False, "message": str(e)}), 502
except Exception as e:
return jsonify({"success": False, "message": f"Unexpected error in button4: {e}"}), 500
return jsonify({
"success": True,
"message": f"Retrieved progress for {len(participant_to_progress_dict)} subjects",
"study_id": study_id,
"qualtrics_survey_id": study_variables.survey_id,
"embedded_data_field": study_variables.embedded_data_field,
"progress_results": participant_to_progress_dict,
})
# Here need to add the last step to send the progress back to Ldot, but that will be done in a separate function or workflow.
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=int(os.getenv("PORT", 5000)),
debug=os.getenv("FLASK_DEBUG", "false").lower() == "true",
)