From f2ea95d84ebaa6d7aad7f35ad89c59692daf87aa Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Wed, 24 Jan 2024 15:59:36 -0500 Subject: [PATCH 01/11] WIP --- qualtrix/api.py | 11 +++++++++++ qualtrix/client.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/qualtrix/api.py b/qualtrix/api.py index 0a30273..ad23a2b 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -57,6 +57,17 @@ async def get_redirect(request: RedirectModel): raise HTTPException(status_code=400, detail=e.args) +@router.post("/redirect-v2") +async def get_redirect_v2(request: RedirectModel): + try: + participant = client.get_participant(request.surveyId, request.responseId) + return participant + except error.QualtricsError as e: + logging.error(e) + # the next time any client side changes are required update this to 422 + raise HTTPException(status_code=400, detail=e.args) + + @router.post("/survey-schema") async def get_schema(request: SurveyModel): return client.get_survey_schema(request.surveyId) diff --git a/qualtrix/client.py b/qualtrix/client.py index 7ede651..d3ce9c1 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -58,6 +58,29 @@ def __eq__(self, other): return self.value == other +def get_participant(survey_id: str, response_id: str): + header = copy.deepcopy(auth_header) + header["Accept"] = "application/json" + + logging.info( + f"Survey, Response -> Email (SurveyId={survey_id}, Response={response_id})" + ) + + # ResponseId -> Email + r = requests.get( + settings.BASE_URL + f"/surveys/{survey_id}/responses/{response_id}", + headers=auth_header, + timeout=settings.TIMEOUT, + ) + + response_id_to_participant = r.json() + + if "error" in response_id_to_participant["meta"]: + raise error.QualtricsError(response_id_to_participant["meta"]["error"]) + + return response_id_to_participant + + def get_email(survey_id: str, response_id: str): header = copy.deepcopy(auth_header) header["Accept"] = "application/json" From 2e0c18fc394808c3e77e5b1c9e12680e565069d0 Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Tue, 30 Jan 2024 14:53:40 -0500 Subject: [PATCH 02/11] Redirect without auth --- qualtrix/api.py | 59 ++++++++++---- qualtrix/client.py | 188 ++++++++++++++++++++++++++++++++++++++++++- qualtrix/main.py | 12 +++ qualtrix/settings.py | 39 ++++++++- 4 files changed, 279 insertions(+), 19 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index ad23a2b..c3efd72 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -2,7 +2,11 @@ qualtrix rest api """ +from asyncio import create_task +import datetime +from datetime import datetime, timedelta import logging +import time import fastapi from fastapi import HTTPException @@ -29,7 +33,10 @@ class SessionModel(SurveyModel): class RedirectModel(SurveyModel): targetSurveyId: str - responseId: str + email: str + first_name: str + last_name: str + auth_token: str @router.post("/bulk-responses") @@ -46,26 +53,46 @@ async def get_response(request: ResponseModel): @router.post("/redirect") -async def get_redirect(request: RedirectModel): +async def intake_redirect(request: RedirectModel): + start_time = time.time() try: - email = client.get_email(request.surveyId, request.responseId) - contact = client.get_contact(settings.DIRECTORY_ID, email) - distribution = client.get_distribution(settings.DIRECTORY_ID, contact["id"]) - return client.get_link(request.targetSurveyId, distribution["distributionId"]) + # participant = client.get_participant(request.surveyId, request.responseId) + directory_entry = client.create_directory_entry( + request.email, + request.first_name, + request.last_name, + settings.DIRECTORY_ID, + settings.MAILING_LIST_ID, + ) + # TODO: Abstract this into a general create_distribution with a type argument + email_distribution = client.create_email_distribution( + directory_entry["contactLookupId"], + settings.DIRECTORY_ID, + settings.LIBRARY_ID, + settings.INVITE_MESSAGE_ID, + settings.MAILING_LIST_ID, + request.targetSurveyId, + ) + link = client.get_link(request.targetSurveyId, email_distribution["id"]) + + # If link creation succeeds, create reminders while the link is returned + create_task(create_reminder_distributions(email_distribution["id"])) + + log.info("Redirect link created in %.2f seconds" % (time.time() - start_time)) + return link except error.QualtricsError as e: logging.error(e) - raise HTTPException(status_code=400, detail=e.args) + # the next time any client side changes are required update this to 422 + raise HTTPException(status_code=422, detail=e.args) -@router.post("/redirect-v2") -async def get_redirect_v2(request: RedirectModel): - try: - participant = client.get_participant(request.surveyId, request.responseId) - return participant - except error.QualtricsError as e: - logging.error(e) - # the next time any client side changes are required update this to 422 - raise HTTPException(status_code=400, detail=e.args) +async def create_reminder_distributions(distribution_id: str): + client.create_reminder_distribution( + settings.LIBRARY_ID, + settings.REMINDER_MESSAGE_ID, + distribution_id, + (datetime.utcnow() + timedelta(minutes=1)), + ) @router.post("/survey-schema") diff --git a/qualtrix/client.py b/qualtrix/client.py index d3ce9c1..1d83ae1 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -4,6 +4,9 @@ import logging import requests import time +import datetime +from datetime import datetime, timedelta + from qualtrix import settings, error @@ -14,6 +17,17 @@ auth_header = {"X-API-TOKEN": settings.API_TOKEN} +class Participant: + def __init__( + self, r_id: str, f_name: str, l_name: str, email: str, lang: str + ) -> None: + self.response_id = r_id + self.first_name = f_name + self.last_name = l_name + self.email = email + self.language = lang + + class IBetaSurveyQuestion(Enum): TESTER_ID = 1 TEST_TYPE = 2 @@ -63,7 +77,7 @@ def get_participant(survey_id: str, response_id: str): header["Accept"] = "application/json" logging.info( - f"Survey, Response -> Email (SurveyId={survey_id}, Response={response_id})" + f"Survey, Response -> Participant (SurveyId={survey_id}, Response={response_id})" ) # ResponseId -> Email @@ -78,7 +92,177 @@ def get_participant(survey_id: str, response_id: str): if "error" in response_id_to_participant["meta"]: raise error.QualtricsError(response_id_to_participant["meta"]["error"]) - return response_id_to_participant + participant_str = response_id_to_participant["result"]["values"] + if participant_str is None: + raise error.QualtricsError( + "Participant not found, did they complete the intake survey?" + ) + + f_name = participant_str["QID37_1"] + l_name = participant_str["QID37_2"] + email = participant_str["QID37_3"] + lang = participant_str["userLanguage"] + + return Participant(response_id, f_name, l_name, email, lang) + + +def create_directory_entry( + email: str, first_name: str, last_name: str, directory_id: str, mailing_list_id: str +): + header = copy.deepcopy(auth_header) + header["Accept"] = "application/json" + + logging.info(f"Creating new directory entry for {email}") + + directory_payload = { + "firstName": first_name, + "lastName": last_name, + "email": email, + "embeddedData": { + "RulesConsentID": "test-rules", + "Date": "test-date", + "time": "test-time", + "SurveyswapID": "", + "utm_source": "test-utmsource", + "utm_medium": "test-utmmedium", + "utm_campaign": "test-utmcampaign", + }, + } + + # Create contact + r = requests.post( + settings.BASE_URL + + f"/directories/{directory_id}/mailinglists/{mailing_list_id}/contacts", + headers=header, + params={"includeEmbedded": "true"}, + json=directory_payload, + timeout=settings.TIMEOUT, + ) + + create_directory_entry_response = r.json() + if "error" in create_directory_entry_response["meta"]: + raise error.QualtricsError(create_directory_entry_response["meta"]["error"]) + + directory_entry = create_directory_entry_response.get("result", None) + if directory_entry is None: + raise error.QualtricsError("Something went wrong creating the contact") + + return directory_entry + + +def create_reminder_distribution( + library_id: str, + reminder_message_id: str, + distribution_id: str, + reminder_date: datetime, +): + + header = copy.deepcopy(auth_header) + header["Accept"] = "application/json" + + logging.info( + f"Create reminder distribution for {distribution_id} on {reminder_date}" + ) + + create_reminder_distribution_payload = { + "message": {"libraryId": library_id, "messageId": reminder_message_id}, + "header": { + "fromEmail": settings.FROM_EMAIL, + "replyToEmail": settings.REPLY_TO_EMAIL, + "fromName": settings.FROM_NAME, + "subject": settings.REMINDER_SUBJECT, + }, + "embeddedData": {"property1": "string", "property2": "string"}, + "sendDate": reminder_date.isoformat() + "Z", + } + + r = requests.post( + settings.BASE_URL + f"/distributions/{distribution_id}/reminders", + headers=header, + json=create_reminder_distribution_payload, + timeout=settings.TIMEOUT, + ) + + create_reminder_distribution_response = r.json() + if "error" in create_reminder_distribution_response["meta"]: + raise error.QualtricsError( + create_reminder_distribution_response["meta"]["error"] + ) + + reminder_distribution = create_reminder_distribution_response["result"] + if reminder_distribution is None: + raise error.QualtricsError("Something went wrong creating the distribution") + + +def add_participant_to_contact_list( + auth_token: str, survey_label: str, survey_link: str +): + header = copy.deepcopy(auth_header) + header["Accept"] = "application/json" + + logging.info("Add participant to the contact list") + + add_particpant_payload = { + "embeddedData": {survey_label: survey_link, "auth_token": auth_token} + } + + r = requests.post( + settings.BASE_URL + f"/distributions/{distribution_id}/reminders", + headers=header, + json=create_reminder_distribution_payload, + timeout=settings.TIMEOUT, + ) + + +def create_email_distribution( + contact_id: str, + distribution_id: str, + library_id: str, + message_id: str, + mailing_list_id: str, + survey_id: str, +): + header = copy.deepcopy(auth_header) + header["Accept"] = "application/json" + + logging.info(f"Create email distribution ") + + calltime = datetime.utcnow() + create_distribution_payload = { + "message": {"libraryId": library_id, "messageId": message_id}, + "recipients": {"mailingListId": mailing_list_id, "contactId": contact_id}, + "header": { + "fromEmail": settings.FROM_EMAIL, + "replyToEmail": settings.REPLY_TO_EMAIL, + "fromName": settings.FROM_NAME, + "subject": settings.INVITE_SUBJECT, + }, + "surveyLink": { + "surveyId": survey_id, + "expirationDate": (calltime + timedelta(minutes=5)).isoformat() + + "Z", # 1 month + "type": "Individual", + }, + "embeddedData": {"": ""}, # for some reason this is required + "sendDate": (calltime + timedelta(seconds=10)).isoformat() + "Z", + } + + r = requests.post( + settings.BASE_URL + f"/distributions", + headers=header, + json=create_distribution_payload, + timeout=settings.TIMEOUT, + ) + + create_distribution_response = r.json() + if "error" in create_distribution_response["meta"]: + raise error.QualtricsError(create_distribution_response["meta"]["error"]) + + email_distribution = create_distribution_response["result"] + if email_distribution is None: + raise error.QualtricsError("Something went wrong creating the distribution") + + return email_distribution def get_email(survey_id: str, response_id: str): diff --git a/qualtrix/main.py b/qualtrix/main.py index b412072..6ff0bab 100644 --- a/qualtrix/main.py +++ b/qualtrix/main.py @@ -12,6 +12,18 @@ app = fastapi.FastAPI() +origins = ["*"] + +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + app.add_middleware(starlette_prometheus.PrometheusMiddleware) app.add_route("/metrics/", starlette_prometheus.metrics) diff --git a/qualtrix/settings.py b/qualtrix/settings.py index a226869..7cd4563 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -15,9 +15,26 @@ LOG_LEVEL = os.getenv("LOG_LEVEL", logging.getLevelName(logging.INFO)) +# Qualtrics API Access API_TOKEN = None BASE_URL = None + +# Qualtrics API Control DIRECTORY_ID = None +LIBRARY_ID = None +REMINDER_MESSAGE_ID = None +INVITE_MESSAGE_ID = None +MAILING_LIST_ID = None + +# Distribution Content Config +FROM_EMAIL = None +REPLY_TO_EMAIL = None +FROM_NAME = None + +INVITE_SUBJECT = None +REMINDER_SUBJECT = None +SURVEY_LINK_TYPE = None + try: vcap_services = os.getenv("VCAP_SERVICES") @@ -32,11 +49,31 @@ API_TOKEN = config["api_token"] BASE_URL = config["base_url"] DIRECTORY_ID = config["directory_id"] + LIBRARY_ID = config["library_id"] + REMINDER_MESSAGE_ID = config["reminder_message_id"] + INVITE_MESSAGE_ID = config["invite_message_id"] + MAILING_LIST_ID = config["mailing_list_id"] + FROM_EMAIL = config["from_email"] + REPLY_TO_EMAIL = config["reply_to_email"] + FROM_NAME = config["from_name"] + INVITE_SUBJECT = config["invite_subject"] + REMINDER_SUBJECT = config["reminder_subject"] + SURVEY_LINK_TYPE = config["survey_link_type"] + else: API_TOKEN = os.getenv("QUALTRIX_API_TOKEN") BASE_URL = os.getenv("QUALTRIX_BASE_URL") DIRECTORY_ID = os.getenv("QUALTRIX_DIRECTORY_ID") - + LIBRARY_ID = os.getenv("QUALTRIX_LIBRARY_ID") + REMINDER_MESSAGE_ID = os.getenv("QUALTRIX_REMINDER_MESSAGE_ID") + INVITE_MESSAGE_ID = os.getenv("QUALTRIX_INVITE_MESSAGE_ID") + MAILING_LIST_ID = os.getenv("QUALTRIX_MAILING_LIST_ID") + FROM_EMAIL = os.getenv("QUALTRIX_FROM_EMAIL") + REPLY_TO_EMAIL = os.getenv("QUALTRIX_REPLY_TO_EMAIL") + FROM_NAME = os.getenv("QUALTRIX_FROM_NAME") + INVITE_SUBJECT = os.getenv("QUALTRIX_INVITE_SUBJECT") + REMINDER_SUBJECT = os.getenv("QUALTRIX_REMINDER_SUBJECT") + SURVEY_LINK_TYPE = os.getenv("QUALTRIX_SURVEY_LINK_TYPE") except (json.JSONDecodeError, KeyError, FileNotFoundError) as err: log.warning("Unable to load credentials from VCAP_SERVICES") From e496e6f809e8e92e6a542f4dfcbbc77ea44e1c0b Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Wed, 31 Jan 2024 10:56:15 -0500 Subject: [PATCH 03/11] Redirect --- qualtrix/api.py | 17 ++++++++++++----- qualtrix/client.py | 22 ++++++++++++++-------- qualtrix/settings.py | 2 ++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index c3efd72..35fe21f 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -36,7 +36,6 @@ class RedirectModel(SurveyModel): email: str first_name: str last_name: str - auth_token: str @router.post("/bulk-responses") @@ -56,7 +55,6 @@ async def get_response(request: ResponseModel): async def intake_redirect(request: RedirectModel): start_time = time.time() try: - # participant = client.get_participant(request.surveyId, request.responseId) directory_entry = client.create_directory_entry( request.email, request.first_name, @@ -64,10 +62,9 @@ async def intake_redirect(request: RedirectModel): settings.DIRECTORY_ID, settings.MAILING_LIST_ID, ) - # TODO: Abstract this into a general create_distribution with a type argument + email_distribution = client.create_email_distribution( directory_entry["contactLookupId"], - settings.DIRECTORY_ID, settings.LIBRARY_ID, settings.INVITE_MESSAGE_ID, settings.MAILING_LIST_ID, @@ -77,6 +74,7 @@ async def intake_redirect(request: RedirectModel): # If link creation succeeds, create reminders while the link is returned create_task(create_reminder_distributions(email_distribution["id"])) + create_task(add_user_to_contact_list(link["link"], directory_entry["id"])) log.info("Redirect link created in %.2f seconds" % (time.time() - start_time)) return link @@ -87,13 +85,22 @@ async def intake_redirect(request: RedirectModel): async def create_reminder_distributions(distribution_id: str): - client.create_reminder_distribution( + distribution = client.create_reminder_distribution( settings.LIBRARY_ID, settings.REMINDER_MESSAGE_ID, distribution_id, (datetime.utcnow() + timedelta(minutes=1)), ) + logging.info(f"created reminder {distribution['distributionId']}") + + +async def add_user_to_contact_list(survey_link: str, contact_id: str): + contact = client.add_participant_to_contact_list( + settings.DEMOGRAPHICS_SURVEY_LABEL, survey_link, contact_id + ) + logging.info(f"add contact {contact} to contact list") + @router.post("/survey-schema") async def get_schema(request: SurveyModel): diff --git a/qualtrix/client.py b/qualtrix/client.py index 1d83ae1..026c8d9 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -193,30 +193,36 @@ def create_reminder_distribution( if reminder_distribution is None: raise error.QualtricsError("Something went wrong creating the distribution") + return reminder_distribution + def add_participant_to_contact_list( - auth_token: str, survey_label: str, survey_link: str + survey_label: str, survey_link: str, contact_id: str ): header = copy.deepcopy(auth_header) header["Accept"] = "application/json" logging.info("Add participant to the contact list") - add_particpant_payload = { - "embeddedData": {survey_label: survey_link, "auth_token": auth_token} - } + add_particpant_payload = {"embeddedData": {survey_label: survey_link}} - r = requests.post( - settings.BASE_URL + f"/distributions/{distribution_id}/reminders", + r = requests.put( + settings.BASE_URL + + f"/directories/{settings.DIRECTORY_ID}/mailinglists/{settings.MAILING_LIST_ID}/contacts/{contact_id}", headers=header, - json=create_reminder_distribution_payload, + json=add_particpant_payload, timeout=settings.TIMEOUT, ) + add_to_contact_list_response = r.json() + if "error" in add_to_contact_list_response["meta"]: + raise error.QualtricsError(add_to_contact_list_response["meta"]["error"]) + + return contact_id + def create_email_distribution( contact_id: str, - distribution_id: str, library_id: str, message_id: str, mailing_list_id: str, diff --git a/qualtrix/settings.py b/qualtrix/settings.py index 7cd4563..a1af691 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -59,6 +59,7 @@ INVITE_SUBJECT = config["invite_subject"] REMINDER_SUBJECT = config["reminder_subject"] SURVEY_LINK_TYPE = config["survey_link_type"] + DEMOGRAPHICS_SURVEY_LABEL = config["demographics_survey_label"] else: API_TOKEN = os.getenv("QUALTRIX_API_TOKEN") @@ -74,6 +75,7 @@ INVITE_SUBJECT = os.getenv("QUALTRIX_INVITE_SUBJECT") REMINDER_SUBJECT = os.getenv("QUALTRIX_REMINDER_SUBJECT") SURVEY_LINK_TYPE = os.getenv("QUALTRIX_SURVEY_LINK_TYPE") + DEMOGRAPHICS_SURVEY_LABEL = os.getenv("QUALTRIX_DEMOGRAPHICS_SURVEY_LABEL") except (json.JSONDecodeError, KeyError, FileNotFoundError) as err: log.warning("Unable to load credentials from VCAP_SERVICES") From 82ccf009e69e72ecf5db689ecb1835a92ae957db Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Wed, 31 Jan 2024 17:42:54 -0500 Subject: [PATCH 04/11] Integration changes --- qualtrix/api.py | 11 +++++------ qualtrix/client.py | 6 ++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index 35fe21f..abea155 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -34,8 +34,8 @@ class SessionModel(SurveyModel): class RedirectModel(SurveyModel): targetSurveyId: str email: str - first_name: str - last_name: str + firstName: str + lastName: str @router.post("/bulk-responses") @@ -57,8 +57,8 @@ async def intake_redirect(request: RedirectModel): try: directory_entry = client.create_directory_entry( request.email, - request.first_name, - request.last_name, + request.firstName, + request.lastName, settings.DIRECTORY_ID, settings.MAILING_LIST_ID, ) @@ -96,10 +96,9 @@ async def create_reminder_distributions(distribution_id: str): async def add_user_to_contact_list(survey_link: str, contact_id: str): - contact = client.add_participant_to_contact_list( + return client.add_participant_to_contact_list( settings.DEMOGRAPHICS_SURVEY_LABEL, survey_link, contact_id ) - logging.info(f"add contact {contact} to contact list") @router.post("/survey-schema") diff --git a/qualtrix/client.py b/qualtrix/client.py index 026c8d9..2aab680 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -202,10 +202,12 @@ def add_participant_to_contact_list( header = copy.deepcopy(auth_header) header["Accept"] = "application/json" - logging.info("Add participant to the contact list") - add_particpant_payload = {"embeddedData": {survey_label: survey_link}} + logging.info( + f"Contact ({contact_id}) -> Directory ({settings.DIRECTORY_ID}), Mailing List ({settings.MAILING_LIST_ID})" + ) + r = requests.put( settings.BASE_URL + f"/directories/{settings.DIRECTORY_ID}/mailinglists/{settings.MAILING_LIST_ID}/contacts/{contact_id}", From 53d239bb0bc5c491f5684ddd8ed54e5e64a02deb Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Wed, 31 Jan 2024 21:21:30 -0500 Subject: [PATCH 05/11] Code Review comments and adding in the Rules Consent id --- qualtrix/api.py | 20 ++++++++++++++------ qualtrix/client.py | 21 +++++++++++++++------ qualtrix/main.py | 12 ------------ qualtrix/settings.py | 2 ++ 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index abea155..5a8b153 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -3,7 +3,6 @@ """ from asyncio import create_task -import datetime from datetime import datetime, timedelta import logging import time @@ -36,6 +35,7 @@ class RedirectModel(SurveyModel): email: str firstName: str lastName: str + rulesConsentId: str @router.post("/bulk-responses") @@ -74,7 +74,11 @@ async def intake_redirect(request: RedirectModel): # If link creation succeeds, create reminders while the link is returned create_task(create_reminder_distributions(email_distribution["id"])) - create_task(add_user_to_contact_list(link["link"], directory_entry["id"])) + create_task( + add_user_to_contact_list( + link["link"], directory_entry["id"], request.rulesConsentId + ) + ) log.info("Redirect link created in %.2f seconds" % (time.time() - start_time)) return link @@ -92,12 +96,16 @@ async def create_reminder_distributions(distribution_id: str): (datetime.utcnow() + timedelta(minutes=1)), ) - logging.info(f"created reminder {distribution['distributionId']}") - -async def add_user_to_contact_list(survey_link: str, contact_id: str): +async def add_user_to_contact_list( + survey_link: str, contact_id: str, rules_consent_id: str +): return client.add_participant_to_contact_list( - settings.DEMOGRAPHICS_SURVEY_LABEL, survey_link, contact_id + settings.DEMOGRAPHICS_SURVEY_LABEL, + settings.RULES_CONSENT_ID_LABEL, + survey_link, + contact_id, + rules_consent_id, ) diff --git a/qualtrix/client.py b/qualtrix/client.py index 2aab680..a3ad646 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -160,10 +160,6 @@ def create_reminder_distribution( header = copy.deepcopy(auth_header) header["Accept"] = "application/json" - logging.info( - f"Create reminder distribution for {distribution_id} on {reminder_date}" - ) - create_reminder_distribution_payload = { "message": {"libraryId": library_id, "messageId": reminder_message_id}, "header": { @@ -193,16 +189,29 @@ def create_reminder_distribution( if reminder_distribution is None: raise error.QualtricsError("Something went wrong creating the distribution") + logging.info( + f"Create reminder distribution ({reminder_distribution['distributionId']}) -> {distribution_id} on {reminder_date}" + ) + return reminder_distribution def add_participant_to_contact_list( - survey_label: str, survey_link: str, contact_id: str + survey_label: str, + rules_consent_id_label, + survey_link: str, + contact_id: str, + rules_consent_id: str, ): header = copy.deepcopy(auth_header) header["Accept"] = "application/json" - add_particpant_payload = {"embeddedData": {survey_label: survey_link}} + add_particpant_payload = { + "embeddedData": { + survey_label: survey_link, + rules_consent_id_label: rules_consent_id, + } + } logging.info( f"Contact ({contact_id}) -> Directory ({settings.DIRECTORY_ID}), Mailing List ({settings.MAILING_LIST_ID})" diff --git a/qualtrix/main.py b/qualtrix/main.py index 6ff0bab..b412072 100644 --- a/qualtrix/main.py +++ b/qualtrix/main.py @@ -12,18 +12,6 @@ app = fastapi.FastAPI() -origins = ["*"] - -from fastapi.middleware.cors import CORSMiddleware - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - app.add_middleware(starlette_prometheus.PrometheusMiddleware) app.add_route("/metrics/", starlette_prometheus.metrics) diff --git a/qualtrix/settings.py b/qualtrix/settings.py index a1af691..1302eac 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -60,6 +60,7 @@ REMINDER_SUBJECT = config["reminder_subject"] SURVEY_LINK_TYPE = config["survey_link_type"] DEMOGRAPHICS_SURVEY_LABEL = config["demographics_survey_label"] + RULES_CONSENT_ID_LABEL = config["rules_consent_id_label"] else: API_TOKEN = os.getenv("QUALTRIX_API_TOKEN") @@ -76,6 +77,7 @@ REMINDER_SUBJECT = os.getenv("QUALTRIX_REMINDER_SUBJECT") SURVEY_LINK_TYPE = os.getenv("QUALTRIX_SURVEY_LINK_TYPE") DEMOGRAPHICS_SURVEY_LABEL = os.getenv("QUALTRIX_DEMOGRAPHICS_SURVEY_LABEL") + RULES_CONSENT_ID_LABEL = os.getenv("QUALTRIX_GET_RULES_CONSENT_ID_LABEL") except (json.JSONDecodeError, KeyError, FileNotFoundError) as err: log.warning("Unable to load credentials from VCAP_SERVICES") From 52eb7c94f24a00979b7d962237d2d518855d2ccf Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Wed, 31 Jan 2024 21:40:50 -0500 Subject: [PATCH 06/11] black --- qualtrix/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qualtrix/client.py b/qualtrix/client.py index a3ad646..2ccaafd 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -156,7 +156,6 @@ def create_reminder_distribution( distribution_id: str, reminder_date: datetime, ): - header = copy.deepcopy(auth_header) header["Accept"] = "application/json" From 697b9c02186b630f66635a01d79a4a7d6328dc05 Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Thu, 1 Feb 2024 17:34:21 -0500 Subject: [PATCH 07/11] Adding some more embedded data --- qualtrix/api.py | 21 +++++++++++++++++---- qualtrix/client.py | 12 +++++++++++- qualtrix/main.py | 15 +++++++++++++++ qualtrix/settings.py | 5 +++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index 5a8b153..64e90a2 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -76,7 +76,12 @@ async def intake_redirect(request: RedirectModel): create_task(create_reminder_distributions(email_distribution["id"])) create_task( add_user_to_contact_list( - link["link"], directory_entry["id"], request.rulesConsentId + link["link"], + directory_entry["id"], + request.rulesConsentId, + request.firstName, + request.lastName, + datetime.utcnow(), ) ) @@ -93,19 +98,27 @@ async def create_reminder_distributions(distribution_id: str): settings.LIBRARY_ID, settings.REMINDER_MESSAGE_ID, distribution_id, - (datetime.utcnow() + timedelta(minutes=1)), + (datetime.utcnow() + timedelta(days=1)), ) async def add_user_to_contact_list( - survey_link: str, contact_id: str, rules_consent_id: str + survey_link: str, + contact_id: str, + rules_consent_id: str, + first_name: str, + last_name: str, + timestamp_utc: datetime, ): return client.add_participant_to_contact_list( settings.DEMOGRAPHICS_SURVEY_LABEL, settings.RULES_CONSENT_ID_LABEL, survey_link, contact_id, - rules_consent_id, + client.modify_prefix("FS", "R", rules_consent_id), + first_name, + last_name, + timestamp_utc, ) diff --git a/qualtrix/client.py b/qualtrix/client.py index 2ccaafd..3addc43 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -195,12 +195,19 @@ def create_reminder_distribution( return reminder_distribution +def modify_prefix(current: str, desired: str, content: str) -> str: + return content.replace(f"{current}_", f"{desired}_", 1) + + def add_participant_to_contact_list( survey_label: str, rules_consent_id_label, survey_link: str, contact_id: str, rules_consent_id: str, + first_name: str, + last_name: str, + timestamp_utc: datetime, ): header = copy.deepcopy(auth_header) header["Accept"] = "application/json" @@ -209,11 +216,14 @@ def add_participant_to_contact_list( "embeddedData": { survey_label: survey_link, rules_consent_id_label: rules_consent_id, + "firstName": first_name, + "lastName": last_name, + "timestamp": timestamp_utc.isoformat() + "Z", } } logging.info( - f"Contact ({contact_id}) -> Directory ({settings.DIRECTORY_ID}), Mailing List ({settings.MAILING_LIST_ID})" + f"Contact ({contact_id}) -> Directory ({settings.DIRECTORY_ID}), Mailing List ({settings.MAILING_LIST_ID}), Rules Consent ({rules_consent_id})" ) r = requests.put( diff --git a/qualtrix/main.py b/qualtrix/main.py index b412072..8da05cb 100644 --- a/qualtrix/main.py +++ b/qualtrix/main.py @@ -12,6 +12,21 @@ app = fastapi.FastAPI() +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +# app = FastAPI() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + app.add_middleware(starlette_prometheus.PrometheusMiddleware) app.add_route("/metrics/", starlette_prometheus.metrics) diff --git a/qualtrix/settings.py b/qualtrix/settings.py index 1302eac..66a2585 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -34,7 +34,8 @@ INVITE_SUBJECT = None REMINDER_SUBJECT = None SURVEY_LINK_TYPE = None - +DEMOGRAPHICS_SURVEY_LABEL = None +RULES_CONSENT_ID_LABEL = None try: vcap_services = os.getenv("VCAP_SERVICES") @@ -77,7 +78,7 @@ REMINDER_SUBJECT = os.getenv("QUALTRIX_REMINDER_SUBJECT") SURVEY_LINK_TYPE = os.getenv("QUALTRIX_SURVEY_LINK_TYPE") DEMOGRAPHICS_SURVEY_LABEL = os.getenv("QUALTRIX_DEMOGRAPHICS_SURVEY_LABEL") - RULES_CONSENT_ID_LABEL = os.getenv("QUALTRIX_GET_RULES_CONSENT_ID_LABEL") + RULES_CONSENT_ID_LABEL = os.getenv("QUALTRIX_RULES_CONSENT_ID_LABEL") except (json.JSONDecodeError, KeyError, FileNotFoundError) as err: log.warning("Unable to load credentials from VCAP_SERVICES") From 58e8d7fe2b773bb8401e6d818ed46b1758853d9c Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Fri, 2 Feb 2024 12:32:58 -0500 Subject: [PATCH 08/11] Adding more embedded data and properly formatting the time --- qualtrix/api.py | 44 ++++++++++++++++++++++++++++++++++++-------- qualtrix/client.py | 28 +++++++++++++++------------- qualtrix/main.py | 15 --------------- qualtrix/settings.py | 3 +++ requirements.txt | 1 + 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index 64e90a2..11c553d 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import logging import time +from zoneinfo import ZoneInfo import fastapi from fastapi import HTTPException @@ -32,10 +33,14 @@ class SessionModel(SurveyModel): class RedirectModel(SurveyModel): targetSurveyId: str + RulesConsentID: str # Client dependent + SurveyswapID: str # Client dependent + utm_campaign: str + utm_medium: str + utm_source: str email: str firstName: str lastName: str - rulesConsentId: str @router.post("/bulk-responses") @@ -70,6 +75,7 @@ async def intake_redirect(request: RedirectModel): settings.MAILING_LIST_ID, request.targetSurveyId, ) + link = client.get_link(request.targetSurveyId, email_distribution["id"]) # If link creation succeeds, create reminders while the link is returned @@ -78,10 +84,16 @@ async def intake_redirect(request: RedirectModel): add_user_to_contact_list( link["link"], directory_entry["id"], - request.rulesConsentId, + request.RulesConsentID, + request.SurveyswapID, + request.utm_campaign, + request.utm_medium, + request.utm_source, request.firstName, request.lastName, - datetime.utcnow(), + # https://stackoverflow.com/questions/10997577/python-timezone-conversion + # Consumers to this data require mountain time + datetime.now(tz=ZoneInfo("MST")), ) ) @@ -98,7 +110,14 @@ async def create_reminder_distributions(distribution_id: str): settings.LIBRARY_ID, settings.REMINDER_MESSAGE_ID, distribution_id, - (datetime.utcnow() + timedelta(days=1)), + (datetime.utcnow() + timedelta(minutes=1)), + ) + + distribution = client.create_reminder_distribution( + settings.LIBRARY_ID, + settings.REMINDER_MESSAGE_ID, + distribution_id, + (datetime.utcnow() + timedelta(minutes=3)), ) @@ -106,19 +125,28 @@ async def add_user_to_contact_list( survey_link: str, contact_id: str, rules_consent_id: str, + survey_swap_id: str, + utm_campaign: str, + utm_medium: str, + utm_source: str, first_name: str, last_name: str, - timestamp_utc: datetime, + timestamp: datetime, ): return client.add_participant_to_contact_list( settings.DEMOGRAPHICS_SURVEY_LABEL, - settings.RULES_CONSENT_ID_LABEL, survey_link, - contact_id, + settings.RULES_CONSENT_ID_LABEL, client.modify_prefix("FS", "R", rules_consent_id), + settings.SURVEY_SWAP_ID_LABEL, + survey_swap_id, + contact_id, + utm_campaign, + utm_medium, + utm_source, first_name, last_name, - timestamp_utc, + timestamp, ) diff --git a/qualtrix/client.py b/qualtrix/client.py index 3addc43..596f583 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -4,6 +4,7 @@ import logging import requests import time +import pytz import datetime from datetime import datetime, timedelta @@ -118,15 +119,6 @@ def create_directory_entry( "firstName": first_name, "lastName": last_name, "email": email, - "embeddedData": { - "RulesConsentID": "test-rules", - "Date": "test-date", - "time": "test-time", - "SurveyswapID": "", - "utm_source": "test-utmsource", - "utm_medium": "test-utmmedium", - "utm_campaign": "test-utmcampaign", - }, } # Create contact @@ -201,13 +193,18 @@ def modify_prefix(current: str, desired: str, content: str) -> str: def add_participant_to_contact_list( survey_label: str, - rules_consent_id_label, survey_link: str, - contact_id: str, + rules_consent_id_label, rules_consent_id: str, + survey_swap_id_label: str, + survey_swap_id, + contact_id: str, + utm_campaign: str, + utm_medium: str, + utm_source: str, first_name: str, last_name: str, - timestamp_utc: datetime, + timestamp: datetime, ): header = copy.deepcopy(auth_header) header["Accept"] = "application/json" @@ -216,9 +213,14 @@ def add_participant_to_contact_list( "embeddedData": { survey_label: survey_link, rules_consent_id_label: rules_consent_id, + survey_swap_id_label: survey_swap_id, + "utm_campaign": utm_campaign, + "utm_medium": utm_medium, + "utm_source": utm_source, "firstName": first_name, "lastName": last_name, - "timestamp": timestamp_utc.isoformat() + "Z", + "Date": timestamp.strftime("%m/%d/%Y"), + "time": timestamp.strftime("%H:%M:%S"), } } diff --git a/qualtrix/main.py b/qualtrix/main.py index 8da05cb..b412072 100644 --- a/qualtrix/main.py +++ b/qualtrix/main.py @@ -12,21 +12,6 @@ app = fastapi.FastAPI() -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -# app = FastAPI() - -origins = ["*"] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - app.add_middleware(starlette_prometheus.PrometheusMiddleware) app.add_route("/metrics/", starlette_prometheus.metrics) diff --git a/qualtrix/settings.py b/qualtrix/settings.py index 66a2585..b7dfbe8 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -36,6 +36,7 @@ SURVEY_LINK_TYPE = None DEMOGRAPHICS_SURVEY_LABEL = None RULES_CONSENT_ID_LABEL = None +SURVEY_SWAP_ID_LABEL = None try: vcap_services = os.getenv("VCAP_SERVICES") @@ -62,6 +63,7 @@ SURVEY_LINK_TYPE = config["survey_link_type"] DEMOGRAPHICS_SURVEY_LABEL = config["demographics_survey_label"] RULES_CONSENT_ID_LABEL = config["rules_consent_id_label"] + SURVEY_SWAP_ID_LABEL = config["survey_swap_id_label"] else: API_TOKEN = os.getenv("QUALTRIX_API_TOKEN") @@ -79,6 +81,7 @@ SURVEY_LINK_TYPE = os.getenv("QUALTRIX_SURVEY_LINK_TYPE") DEMOGRAPHICS_SURVEY_LABEL = os.getenv("QUALTRIX_DEMOGRAPHICS_SURVEY_LABEL") RULES_CONSENT_ID_LABEL = os.getenv("QUALTRIX_RULES_CONSENT_ID_LABEL") + SURVEY_SWAP_ID_LABEL = os.getenv("QUALTRIX_SURVEY_SWAP_ID_LABEL") except (json.JSONDecodeError, KeyError, FileNotFoundError) as err: log.warning("Unable to load credentials from VCAP_SERVICES") diff --git a/requirements.txt b/requirements.txt index fc25a15..1d3a0f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ starlette-prometheus==0.9.0 google-api-python-client==2.113.0 google-auth-httplib2==0.2.0 google-auth-oauthlib==1.2.0 +pytz==2024.1 \ No newline at end of file From ea8aee00cf92a1d9b64e78559ff8a72c23c91f2a Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Fri, 2 Feb 2024 12:34:25 -0500 Subject: [PATCH 09/11] Removing a dep i didnt need --- qualtrix/client.py | 1 - requirements.txt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/qualtrix/client.py b/qualtrix/client.py index 596f583..d149c85 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -4,7 +4,6 @@ import logging import requests import time -import pytz import datetime from datetime import datetime, timedelta diff --git a/requirements.txt b/requirements.txt index 1d3a0f5..79eb457 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,4 @@ uvicorn==0.25.0 starlette-prometheus==0.9.0 google-api-python-client==2.113.0 google-auth-httplib2==0.2.0 -google-auth-oauthlib==1.2.0 -pytz==2024.1 \ No newline at end of file +google-auth-oauthlib==1.2.0 \ No newline at end of file From 7c3699630921768835758589c8506e714b115a63 Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Fri, 2 Feb 2024 14:54:45 -0500 Subject: [PATCH 10/11] formatting --- .pre-commit-config.yaml | 4 ++-- qualtrix/client.py | 36 ++++++++++++++++++------------------ qualtrix/main.py | 1 + qualtrix/settings.py | 1 + 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3998f3..2fdcb3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.3.0 # Update with 'pre-commit autoupdate' + rev: 24.1.1 # Update with 'pre-commit autoupdate' hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.7 hooks: - id: bandit exclude: tests diff --git a/qualtrix/client.py b/qualtrix/client.py index d149c85..f9370ed 100644 --- a/qualtrix/client.py +++ b/qualtrix/client.py @@ -565,26 +565,26 @@ def get_answer_from_result(result): } if device_type_choice == 1: # Iphone or Ipad - device_response[ - "device_model" - ] = IBetaSurveyQuestion.DEVICE_MODEL_APPLE.QID_label(labels) - device_response[ - "device_details" - ] = IBetaSurveyQuestion.DEVICE_MODEL_APPLE.QID_text(values) + device_response["device_model"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_APPLE.QID_label(labels) + ) + device_response["device_details"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_APPLE.QID_text(values) + ) elif device_type_choice == 2: # Samsung Galaxy Phone or Tablet - device_response[ - "device_model" - ] = IBetaSurveyQuestion.DEVICE_MODEL_SAMSUNG.QID_label(labels) - device_response[ - "device_details" - ] = IBetaSurveyQuestion.DEVICE_MODEL_SAMSUNG.QID_text(values) + device_response["device_model"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_SAMSUNG.QID_label(labels) + ) + device_response["device_details"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_SAMSUNG.QID_text(values) + ) elif device_type_choice == 3: # Google Phone or Tablet - device_response[ - "device_model" - ] = IBetaSurveyQuestion.DEVICE_MODEL_GOOGLE.QID_label(labels) - device_response[ - "device_details" - ] = IBetaSurveyQuestion.DEVICE_MODEL_GOOGLE.QID_text(values) + device_response["device_model"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_GOOGLE.QID_label(labels) + ) + device_response["device_details"] = ( + IBetaSurveyQuestion.DEVICE_MODEL_GOOGLE.QID_text(values) + ) return { "tester_id": IBetaSurveyQuestion.TESTER_ID.QID_label(labels), diff --git a/qualtrix/main.py b/qualtrix/main.py index b412072..441668f 100644 --- a/qualtrix/main.py +++ b/qualtrix/main.py @@ -1,6 +1,7 @@ """ Qualtrix Microservice FastAPI Web App. """ + import logging import fastapi diff --git a/qualtrix/settings.py b/qualtrix/settings.py index b7dfbe8..889781e 100644 --- a/qualtrix/settings.py +++ b/qualtrix/settings.py @@ -2,6 +2,7 @@ Configuration for the qualtrix microservice settings. Context is switched based on if the app is in debug mode. """ + import json import logging import os From aabb21c27a3034d69c2e24b2b8150d727f356f2f Mon Sep 17 00:00:00 2001 From: nathan-moore-97 Date: Fri, 2 Feb 2024 14:56:00 -0500 Subject: [PATCH 11/11] Days not minutes --- qualtrix/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qualtrix/api.py b/qualtrix/api.py index 11c553d..b679006 100644 --- a/qualtrix/api.py +++ b/qualtrix/api.py @@ -110,14 +110,14 @@ async def create_reminder_distributions(distribution_id: str): settings.LIBRARY_ID, settings.REMINDER_MESSAGE_ID, distribution_id, - (datetime.utcnow() + timedelta(minutes=1)), + (datetime.utcnow() + timedelta(days=1)), ) distribution = client.create_reminder_distribution( settings.LIBRARY_ID, settings.REMINDER_MESSAGE_ID, distribution_id, - (datetime.utcnow() + timedelta(minutes=3)), + (datetime.utcnow() + timedelta(days=3)), )