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")