From b19171d32411f50759506009aab4640803c87883 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 10 Jan 2025 10:40:38 +0100 Subject: [PATCH 1/5] wip --- src/apps/answers/domain/answers.py | 2 ++ src/apps/answers/service.py | 11 +++++++++++ src/apps/integrations/prolific/domain.py | 5 +++++ src/apps/subjects/crud/subject.py | 1 + 4 files changed, 19 insertions(+) diff --git a/src/apps/answers/domain/answers.py b/src/apps/answers/domain/answers.py index e17820ec2c69..caf4dbacef22 100644 --- a/src/apps/answers/domain/answers.py +++ b/src/apps/answers/domain/answers.py @@ -17,6 +17,7 @@ from apps.activity_flows.domain.flow_full import FlowFull, FlowHistoryWithActivityFlat, FlowHistoryWithActivityFull from apps.answers.domain.answer_items import AnswerItem, ItemAnswerCreate from apps.applets.domain.base import AppletBaseInfo +from apps.integrations.prolific.domain import ProlificAnswerStudyContext from apps.shared.domain import InternalModel, PublicModel, Response from apps.shared.domain.custom_validations import datetime_from_ms from apps.shared.domain.types import _BaseModel @@ -113,6 +114,7 @@ class AppletAnswerCreate(InternalModel): source_subject_id: uuid.UUID | None = None input_subject_id: uuid.UUID | None = None consent_to_share: bool | None = False + prolific_params: ProlificAnswerStudyContext | None = None _dates_from_ms = validator("created_at", pre=True, allow_reuse=True)(datetime_from_ms) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index c468a624b11c..7ff239a09745 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -316,6 +316,17 @@ async def _create_answer(self, applet_answer: AppletAnswerCreate) -> AnswerSchem ) if not respondent_subject or not respondent_subject.soft_exists(): raise ValidationError("Respondent subject not found") + + if applet_answer.prolific_params: + print(respondent_subject.id) + await subject_crud.update_by_id( + respondent_subject.id, + **dict({ + "secret_user_id":f"ProlificPID:{applet_answer.prolific_params.prolific_pid}\ + /SessionID:{applet_answer.prolific_params.session_id}\ + /StudyID:{applet_answer.prolific_params.study_id}" + }) + ) if applet_answer.input_subject_id: input_subject = await subject_crud.get_by_id(applet_answer.input_subject_id) diff --git a/src/apps/integrations/prolific/domain.py b/src/apps/integrations/prolific/domain.py index bf6c38fedbc2..0371ee9fe53b 100644 --- a/src/apps/integrations/prolific/domain.py +++ b/src/apps/integrations/prolific/domain.py @@ -3,6 +3,7 @@ from pydantic import BaseModel from apps.integrations.db.schemas import IntegrationsSchema +from apps.shared.domain.base import InternalModel class ProlificIntegration(BaseModel): @@ -15,3 +16,7 @@ def from_schema(cls, schema: IntegrationsSchema): def __repr__(self): return "ProlificIntegration()" +class ProlificAnswerStudyContext(InternalModel): + prolific_pid: str + session_id: str + study_id: str diff --git a/src/apps/subjects/crud/subject.py b/src/apps/subjects/crud/subject.py index 57971b667aa0..d16102d95b11 100644 --- a/src/apps/subjects/crud/subject.py +++ b/src/apps/subjects/crud/subject.py @@ -28,6 +28,7 @@ async def update(self, schema: SubjectSchema) -> SubjectSchema: return await self._update_one("id", schema.id, schema) async def update_by_id(self, id_, **values): + print(values) query = ( update(self.schema_class).where(self.schema_class.id == id_).values(**values).returning(self.schema_class) ) From ca0141077e58c486769aaeb7a85a4339f59f2a80 Mon Sep 17 00:00:00 2001 From: Hamza Date: Tue, 14 Jan 2025 14:26:38 +0100 Subject: [PATCH 2/5] wip --- src/apps/integrations/prolific/api.py | 23 ++++++++++++ src/apps/integrations/prolific/domain.py | 12 +++++++ src/apps/integrations/prolific/router.py | 35 +++++++++++++++++++ .../integrations/prolific/service/prolific.py | 35 +++++++++++++++++-- 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/apps/integrations/prolific/api.py create mode 100644 src/apps/integrations/prolific/router.py diff --git a/src/apps/integrations/prolific/api.py b/src/apps/integrations/prolific/api.py new file mode 100644 index 000000000000..3d19f492fca0 --- /dev/null +++ b/src/apps/integrations/prolific/api.py @@ -0,0 +1,23 @@ +import uuid + +from fastapi import Depends +from apps.authentication.deps import get_current_user +from apps.integrations.prolific.domain import PublicProlificIntegration +from apps.integrations.prolific.service.prolific import ProlificIntegrationService +from apps.users.domain import User +from infrastructure.database.deps import get_session + + +async def prolific_integration_exists( + applet_id: uuid.UUID, + user: User = Depends(get_current_user), + session=Depends(get_session)) -> PublicProlificIntegration: + exists = await ProlificIntegrationService(session=session, user=user, applet_id=applet_id).prolific_integration_exists() + return PublicProlificIntegration(exists=exists) + +async def get_study_completion_codes( + study_id: str, + applet_id: uuid.UUID, + session=Depends(get_session)) -> list[str]: + print(study_id) + return await ProlificIntegrationService(session=session, applet_id=applet_id).get_completion_codes(study_id) \ No newline at end of file diff --git a/src/apps/integrations/prolific/domain.py b/src/apps/integrations/prolific/domain.py index 0371ee9fe53b..c7ab020ed169 100644 --- a/src/apps/integrations/prolific/domain.py +++ b/src/apps/integrations/prolific/domain.py @@ -20,3 +20,15 @@ class ProlificAnswerStudyContext(InternalModel): prolific_pid: str session_id: str study_id: str + +class ProlificAction(InternalModel): + action: str + +class ProlificCompletionCode(InternalModel): + code: str + code_type: str + actions: list[ProlificAction] + actor: str + +class ProlificCompletionCodeList(InternalModel): + completion_codes: list[ProlificCompletionCode] diff --git a/src/apps/integrations/prolific/router.py b/src/apps/integrations/prolific/router.py new file mode 100644 index 000000000000..504ea52e576f --- /dev/null +++ b/src/apps/integrations/prolific/router.py @@ -0,0 +1,35 @@ +from fastapi.routing import APIRouter + +from starlette import status + +from apps.integrations.prolific.api import get_study_completion_codes, prolific_integration_exists +from apps.integrations.prolific.domain import ProlificCompletionCodeList, PublicProlificIntegration +from apps.shared.domain import Response +from apps.shared.domain.response.errors import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE + + +router = APIRouter(prefix="/integrations/prolific", tags=["Prolific"]) + +router.get( + "", + description="This endpoint is used to check if a prolific integration exists", + response_model=None, + status_code=status.HTTP_200_OK, + responses= { + status.HTTP_200_OK: {"model": Response[PublicProlificIntegration]}, + **DEFAULT_OPENAPI_RESPONSE, + **AUTHENTICATION_ERROR_RESPONSES + }, +)(prolific_integration_exists) + +router.get( + "/applet/{applet_id}/completion_codes/{study_id}", + description="This endpoint is used to get completion codes for a study", + response_model=None, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": ProlificCompletionCodeList}, + **DEFAULT_OPENAPI_RESPONSE, + **AUTHENTICATION_ERROR_RESPONSES, + }, +)(get_study_completion_codes) \ No newline at end of file diff --git a/src/apps/integrations/prolific/service/prolific.py b/src/apps/integrations/prolific/service/prolific.py index 882a922b96f9..485ed3cfc45d 100644 --- a/src/apps/integrations/prolific/service/prolific.py +++ b/src/apps/integrations/prolific/service/prolific.py @@ -1,3 +1,5 @@ +import ast +from http.client import HTTPException import uuid import requests @@ -5,13 +7,13 @@ from apps.integrations.crud.integrations import IntegrationsCRUD from apps.integrations.db.schemas import IntegrationsSchema from apps.integrations.domain import AvailableIntegrations -from apps.integrations.prolific.domain import ProlificIntegration from apps.integrations.prolific.errors import ProlificInvalidApiTokenError +from apps.integrations.prolific.domain import ProlificCompletionCode, ProlificCompletionCodeList, ProlificIntegration from apps.users.domain import User class ProlificIntegrationService: - def __init__(self, applet_id: uuid.UUID, session, user: User) -> None: + def __init__(self, applet_id: uuid.UUID, session, user: User = None) -> None: self.applet_id = applet_id self.session = session self.user = user @@ -37,3 +39,32 @@ async def create_prolific_integration(self, api_key: str) -> ProlificIntegration ) return ProlificIntegration.from_schema(integration_schema) + + async def get_completion_codes(self, study_id: str) -> ProlificCompletionCodeList: + integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( + applet_id=self.applet_id, + integration_type=self.type + ) + + api_key = ast.literal_eval(integration.configuration)["api_key"] + + prolific_response = requests.get( + f"https://api.prolific.com/api/v1/studies/{study_id}/", + headers={ + "Authorization": f"Token {api_key}", + "Content-Type": "application/json" + }) + + if (prolific_response.status_code != 200): + raise HTTPException(status_code=prolific_response.status_code, detail=prolific_response.detail) + + prolific_completion_codes = prolific_response.json()["completion_codes"] + print(prolific_completion_codes) + completion_codes = [] + for code in prolific_completion_codes: + completion_codes.append(ProlificCompletionCode(code=code["code"], code_type=code["code_type"], actions=code["actions"], actor=code["actor"])) + + print(completion_codes) + tmp = ProlificCompletionCodeList(completion_codes=prolific_response.json()["completion_codes"]) + print(tmp) + return tmp From c47acb52e5d7da77d168fc85e6a583bab988fa4a Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 22 Jan 2025 16:10:07 +0100 Subject: [PATCH 3/5] wip --- src/apps/integrations/prolific/api.py | 16 ++++++++-------- src/apps/integrations/prolific/domain.py | 4 ++++ src/apps/integrations/prolific/router.py | 18 +++++------------- .../integrations/prolific/service/prolific.py | 15 ++++++++++++++- src/infrastructure/app.py | 2 ++ 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/apps/integrations/prolific/api.py b/src/apps/integrations/prolific/api.py index 3d19f492fca0..dc0169f7bd74 100644 --- a/src/apps/integrations/prolific/api.py +++ b/src/apps/integrations/prolific/api.py @@ -6,18 +6,18 @@ from apps.integrations.prolific.service.prolific import ProlificIntegrationService from apps.users.domain import User from infrastructure.database.deps import get_session +from infrastructure.http.deps import get_language -async def prolific_integration_exists( - applet_id: uuid.UUID, - user: User = Depends(get_current_user), - session=Depends(get_session)) -> PublicProlificIntegration: - exists = await ProlificIntegrationService(session=session, user=user, applet_id=applet_id).prolific_integration_exists() - return PublicProlificIntegration(exists=exists) +async def get_public_prolific_integration( + applet_id: uuid.UUID, + language: str = Depends(get_language), + session=Depends(get_session), +) -> PublicProlificIntegration: + return await ProlificIntegrationService(session=session, applet_id=applet_id).get_public_prolific_integration(language) async def get_study_completion_codes( study_id: str, applet_id: uuid.UUID, session=Depends(get_session)) -> list[str]: - print(study_id) - return await ProlificIntegrationService(session=session, applet_id=applet_id).get_completion_codes(study_id) \ No newline at end of file + return await ProlificIntegrationService(session=session, applet_id=applet_id).get_completion_codes(study_id) diff --git a/src/apps/integrations/prolific/domain.py b/src/apps/integrations/prolific/domain.py index c7ab020ed169..7b41427ae288 100644 --- a/src/apps/integrations/prolific/domain.py +++ b/src/apps/integrations/prolific/domain.py @@ -16,6 +16,10 @@ def from_schema(cls, schema: IntegrationsSchema): def __repr__(self): return "ProlificIntegration()" + +class PublicProlificIntegration(InternalModel): + enabled: bool + class ProlificAnswerStudyContext(InternalModel): prolific_pid: str session_id: str diff --git a/src/apps/integrations/prolific/router.py b/src/apps/integrations/prolific/router.py index 504ea52e576f..af5ccc2f6fa2 100644 --- a/src/apps/integrations/prolific/router.py +++ b/src/apps/integrations/prolific/router.py @@ -2,25 +2,17 @@ from starlette import status -from apps.integrations.prolific.api import get_study_completion_codes, prolific_integration_exists -from apps.integrations.prolific.domain import ProlificCompletionCodeList, PublicProlificIntegration -from apps.shared.domain import Response +from apps.integrations.prolific.api import get_public_prolific_integration, get_study_completion_codes +from apps.integrations.prolific.domain import ProlificCompletionCodeList from apps.shared.domain.response.errors import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE router = APIRouter(prefix="/integrations/prolific", tags=["Prolific"]) router.get( - "", - description="This endpoint is used to check if a prolific integration exists", - response_model=None, - status_code=status.HTTP_200_OK, - responses= { - status.HTTP_200_OK: {"model": Response[PublicProlificIntegration]}, - **DEFAULT_OPENAPI_RESPONSE, - **AUTHENTICATION_ERROR_RESPONSES - }, -)(prolific_integration_exists) + "/applet/{applet_id}", + description="This endpoint is used to get get the prolific configuration for an applet", +)(get_public_prolific_integration) router.get( "/applet/{applet_id}/completion_codes/{study_id}", diff --git a/src/apps/integrations/prolific/service/prolific.py b/src/apps/integrations/prolific/service/prolific.py index 485ed3cfc45d..a317d572f81d 100644 --- a/src/apps/integrations/prolific/service/prolific.py +++ b/src/apps/integrations/prolific/service/prolific.py @@ -4,11 +4,12 @@ import requests +from apps.applets.service.applet import AppletService from apps.integrations.crud.integrations import IntegrationsCRUD from apps.integrations.db.schemas import IntegrationsSchema from apps.integrations.domain import AvailableIntegrations from apps.integrations.prolific.errors import ProlificInvalidApiTokenError -from apps.integrations.prolific.domain import ProlificCompletionCode, ProlificCompletionCodeList, ProlificIntegration +from apps.integrations.prolific.domain import ProlificCompletionCode, ProlificCompletionCodeList, ProlificIntegration, PublicProlificIntegration from apps.users.domain import User @@ -40,6 +41,18 @@ async def create_prolific_integration(self, api_key: str) -> ProlificIntegration return ProlificIntegration.from_schema(integration_schema) + async def get_public_prolific_integration(self, language) -> PublicProlificIntegration: + applet_service = AppletService(self.session, uuid.UUID("00000000-0000-0000-0000-000000000000")) + await applet_service.exist_by_key(self.applet_id) + applet_base_info = await applet_service.get_info_by_key(self.applet_id, language) + + integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( + applet_id=applet_base_info.id, + integration_type=self.type + ) + + return PublicProlificIntegration(enabled=integration is not None) + async def get_completion_codes(self, study_id: str) -> ProlificCompletionCodeList: integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( applet_id=self.applet_id, diff --git a/src/infrastructure/app.py b/src/infrastructure/app.py index b38807cace0b..e7a693f82640 100644 --- a/src/infrastructure/app.py +++ b/src/infrastructure/app.py @@ -17,6 +17,7 @@ import apps.file.router as files import apps.folders.router as folders import apps.healthcheck.router as healthcheck +import apps.integrations.prolific.router as prolific import apps.integrations.loris.router as loris import apps.integrations.router as integrations import apps.invitations.router as invitations @@ -71,6 +72,7 @@ activity_assignments.router, activity_assignments.user_router, loris.router, + prolific.router, integrations.router, ) From 4fbc84e166ca8e00f722e6eb747ff63c4c773af5 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 24 Jan 2025 15:06:13 +0100 Subject: [PATCH 4/5] wip --- src/apps/integrations/prolific/api.py | 2 - src/apps/integrations/prolific/router.py | 11 +++- .../integrations/prolific/service/prolific.py | 10 +--- .../prolific/tests/test_prolific_service.py | 57 +++++++++++++++++++ src/apps/integrations/service/integrations.py | 1 - 5 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 src/apps/integrations/prolific/tests/test_prolific_service.py diff --git a/src/apps/integrations/prolific/api.py b/src/apps/integrations/prolific/api.py index dc0169f7bd74..1d56cdf109b2 100644 --- a/src/apps/integrations/prolific/api.py +++ b/src/apps/integrations/prolific/api.py @@ -1,10 +1,8 @@ import uuid from fastapi import Depends -from apps.authentication.deps import get_current_user from apps.integrations.prolific.domain import PublicProlificIntegration from apps.integrations.prolific.service.prolific import ProlificIntegrationService -from apps.users.domain import User from infrastructure.database.deps import get_session from infrastructure.http.deps import get_language diff --git a/src/apps/integrations/prolific/router.py b/src/apps/integrations/prolific/router.py index af5ccc2f6fa2..bccd4f2deec0 100644 --- a/src/apps/integrations/prolific/router.py +++ b/src/apps/integrations/prolific/router.py @@ -3,7 +3,7 @@ from starlette import status from apps.integrations.prolific.api import get_public_prolific_integration, get_study_completion_codes -from apps.integrations.prolific.domain import ProlificCompletionCodeList +from apps.integrations.prolific.domain import ProlificCompletionCodeList, PublicProlificIntegration from apps.shared.domain.response.errors import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE @@ -12,6 +12,13 @@ router.get( "/applet/{applet_id}", description="This endpoint is used to get get the prolific configuration for an applet", + response_model=None, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": PublicProlificIntegration}, + **DEFAULT_OPENAPI_RESPONSE, + **AUTHENTICATION_ERROR_RESPONSES, + } )(get_public_prolific_integration) router.get( @@ -24,4 +31,4 @@ **DEFAULT_OPENAPI_RESPONSE, **AUTHENTICATION_ERROR_RESPONSES, }, -)(get_study_completion_codes) \ No newline at end of file +)(get_study_completion_codes) diff --git a/src/apps/integrations/prolific/service/prolific.py b/src/apps/integrations/prolific/service/prolific.py index a317d572f81d..747a87b0f094 100644 --- a/src/apps/integrations/prolific/service/prolific.py +++ b/src/apps/integrations/prolific/service/prolific.py @@ -14,10 +14,9 @@ class ProlificIntegrationService: - def __init__(self, applet_id: uuid.UUID, session, user: User = None) -> None: + def __init__(self, applet_id: uuid.UUID, session) -> None: self.applet_id = applet_id self.session = session - self.user = user self.type = AvailableIntegrations.PROLIFIC async def create_prolific_integration(self, api_key: str) -> ProlificIntegration: @@ -72,12 +71,9 @@ async def get_completion_codes(self, study_id: str) -> ProlificCompletionCodeLis raise HTTPException(status_code=prolific_response.status_code, detail=prolific_response.detail) prolific_completion_codes = prolific_response.json()["completion_codes"] - print(prolific_completion_codes) + completion_codes = [] for code in prolific_completion_codes: completion_codes.append(ProlificCompletionCode(code=code["code"], code_type=code["code_type"], actions=code["actions"], actor=code["actor"])) - print(completion_codes) - tmp = ProlificCompletionCodeList(completion_codes=prolific_response.json()["completion_codes"]) - print(tmp) - return tmp + return ProlificCompletionCodeList(completion_codes=prolific_response.json()["completion_codes"]) diff --git a/src/apps/integrations/prolific/tests/test_prolific_service.py b/src/apps/integrations/prolific/tests/test_prolific_service.py new file mode 100644 index 000000000000..1276881ad0a1 --- /dev/null +++ b/src/apps/integrations/prolific/tests/test_prolific_service.py @@ -0,0 +1,57 @@ + +from unittest.mock import AsyncMock, patch +import uuid + +from apps.shared.test.base import BaseTest +from apps.shared.test.client import TestClient +from apps.users.domain import User + +from apps.integrations.prolific.router import router as prolific_router + +class TestProlificService(BaseTest): + + @patch('apps.integrations.prolific.service.prolific.requests.get') + async def test_get_completion_codes(self, mock_get, client: TestClient, user: User, uuid_zero: uuid.UUID): + mock_response = AsyncMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "completion_codes": [ + { + "code": "code1", + "code_type": "type1", + "actions": ["action1"], + "actor": "actor1" + }, + { + "code": "code2", + "code_type": "type2", + "actions": ["action2"], + "actor": "actor2" + } + ] + } + mock_get.return_value = mock_response + + study_id = "study123" + client.login(user) + + completion_codes_url = f"/integrations/prolific/applet/{uuid_zero}/completion_codes/{study_id}" + result = await client.get(completion_codes_url) + + assert len(result.completion_codes) == 2 + assert result.completion_codes[0].code == "code1" + assert result.completion_codes[1].code_type == "type2" + + @patch('apps.integrations.prolific.service.prolific.requests.get') + async def test_get_completion_codes_failure(self, mock_get, client: TestClient, user: User, uuid_zero: uuid.UUID): + mock_response = AsyncMock() + mock_response.status_code = 404 + mock_response.json.return_value = {"detail": "Not found"} + mock_get.return_value = mock_response + + study_id = "study123" + client.login(user) + completion_codes_url = f"/integrations/prolific/applet/{uuid_zero}/completion_codes/{study_id}" + result = await client.get(completion_codes_url) + + assert result.status_code == 404 \ No newline at end of file diff --git a/src/apps/integrations/service/integrations.py b/src/apps/integrations/service/integrations.py index a4341b040c41..a58e31d08039 100644 --- a/src/apps/integrations/service/integrations.py +++ b/src/apps/integrations/service/integrations.py @@ -55,7 +55,6 @@ async def create_integration(self, newIntegration: Integration) -> Integration: await ProlificIntegrationService( newIntegration.applet_id, self.session, - self.user, ).create_prolific_integration(api_key=newIntegration.configuration["api_key"]) return Integration( integration_type=AvailableIntegrations.PROLIFIC, From 0007ced7266a242579b7dede0a4cc8d2d3d1347e Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 29 Jan 2025 18:42:33 +0100 Subject: [PATCH 5/5] wip --- src/apps/answers/domain/answers.py | 4 +- src/apps/answers/service.py | 1 - src/apps/integrations/prolific/api.py | 3 +- src/apps/integrations/prolific/domain.py | 2 +- src/apps/integrations/prolific/router.py | 6 +- .../integrations/prolific/service/prolific.py | 66 +++++++++++-------- src/apps/subjects/crud/subject.py | 1 - 7 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/apps/answers/domain/answers.py b/src/apps/answers/domain/answers.py index caf4dbacef22..5d151613276a 100644 --- a/src/apps/answers/domain/answers.py +++ b/src/apps/answers/domain/answers.py @@ -17,7 +17,7 @@ from apps.activity_flows.domain.flow_full import FlowFull, FlowHistoryWithActivityFlat, FlowHistoryWithActivityFull from apps.answers.domain.answer_items import AnswerItem, ItemAnswerCreate from apps.applets.domain.base import AppletBaseInfo -from apps.integrations.prolific.domain import ProlificAnswerStudyContext +from apps.integrations.prolific.domain import ProlificParamsActivityAnswer from apps.shared.domain import InternalModel, PublicModel, Response from apps.shared.domain.custom_validations import datetime_from_ms from apps.shared.domain.types import _BaseModel @@ -114,7 +114,7 @@ class AppletAnswerCreate(InternalModel): source_subject_id: uuid.UUID | None = None input_subject_id: uuid.UUID | None = None consent_to_share: bool | None = False - prolific_params: ProlificAnswerStudyContext | None = None + prolific_params: ProlificParamsActivityAnswer | None = None _dates_from_ms = validator("created_at", pre=True, allow_reuse=True)(datetime_from_ms) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 7ff239a09745..a65f12a7141c 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -318,7 +318,6 @@ async def _create_answer(self, applet_answer: AppletAnswerCreate) -> AnswerSchem raise ValidationError("Respondent subject not found") if applet_answer.prolific_params: - print(respondent_subject.id) await subject_crud.update_by_id( respondent_subject.id, **dict({ diff --git a/src/apps/integrations/prolific/api.py b/src/apps/integrations/prolific/api.py index 1d56cdf109b2..f652413b5d7a 100644 --- a/src/apps/integrations/prolific/api.py +++ b/src/apps/integrations/prolific/api.py @@ -9,10 +9,11 @@ async def get_public_prolific_integration( applet_id: uuid.UUID, + study_id: str, language: str = Depends(get_language), session=Depends(get_session), ) -> PublicProlificIntegration: - return await ProlificIntegrationService(session=session, applet_id=applet_id).get_public_prolific_integration(language) + return await ProlificIntegrationService(session=session, applet_id=applet_id).get_public_prolific_integration(study_id, language) async def get_study_completion_codes( study_id: str, diff --git a/src/apps/integrations/prolific/domain.py b/src/apps/integrations/prolific/domain.py index 7b41427ae288..98989d12a245 100644 --- a/src/apps/integrations/prolific/domain.py +++ b/src/apps/integrations/prolific/domain.py @@ -20,7 +20,7 @@ def __repr__(self): class PublicProlificIntegration(InternalModel): enabled: bool -class ProlificAnswerStudyContext(InternalModel): +class ProlificParamsActivityAnswer(InternalModel): prolific_pid: str session_id: str study_id: str diff --git a/src/apps/integrations/prolific/router.py b/src/apps/integrations/prolific/router.py index bccd4f2deec0..71aa9a83f955 100644 --- a/src/apps/integrations/prolific/router.py +++ b/src/apps/integrations/prolific/router.py @@ -10,8 +10,8 @@ router = APIRouter(prefix="/integrations/prolific", tags=["Prolific"]) router.get( - "/applet/{applet_id}", - description="This endpoint is used to get get the prolific configuration for an applet", + "/applet/{applet_id}/study_id/{study_id}", + description="This endpoint is used to get the prolific configuration for an applet", response_model=None, status_code=status.HTTP_200_OK, responses={ @@ -23,7 +23,7 @@ router.get( "/applet/{applet_id}/completion_codes/{study_id}", - description="This endpoint is used to get completion codes for a study", + description="This endpoint is used to get the list of completion codes for a given study", response_model=None, status_code=status.HTTP_200_OK, responses={ diff --git a/src/apps/integrations/prolific/service/prolific.py b/src/apps/integrations/prolific/service/prolific.py index 747a87b0f094..44bc0b5b9510 100644 --- a/src/apps/integrations/prolific/service/prolific.py +++ b/src/apps/integrations/prolific/service/prolific.py @@ -1,7 +1,9 @@ import ast from http.client import HTTPException +import json import uuid +from pydantic import ValidationError import requests from apps.applets.service.applet import AppletService @@ -10,8 +12,9 @@ from apps.integrations.domain import AvailableIntegrations from apps.integrations.prolific.errors import ProlificInvalidApiTokenError from apps.integrations.prolific.domain import ProlificCompletionCode, ProlificCompletionCodeList, ProlificIntegration, PublicProlificIntegration -from apps.users.domain import User +PROLIFIC_API_BASE_URL = "https://api.prolific.com/api/v1" +BASE_HEADERS = {"Content-Type": "application/json"} class ProlificIntegrationService: def __init__(self, applet_id: uuid.UUID, session) -> None: @@ -21,8 +24,11 @@ def __init__(self, applet_id: uuid.UUID, session) -> None: async def create_prolific_integration(self, api_key: str) -> ProlificIntegration: prolific_response = requests.get( - "https://api.prolific.com/api/v1/users/me/", - headers={"Authorization": f"Token {api_key}", "Content-Type": "application/json"}, + f"{PROLIFIC_API_BASE_URL}/users/me/", + headers={ + **BASE_HEADERS, + "Authorization": f"Token {api_key}" + }, ) if prolific_response.status_code != 200: @@ -32,48 +38,54 @@ async def create_prolific_integration(self, api_key: str) -> ProlificIntegration IntegrationsSchema( applet_id=self.applet_id, type=self.type, - configuration={ - "api_key": api_key, - }, + configuration=ProlificIntegration(api_key=api_key).json(), ) ) return ProlificIntegration.from_schema(integration_schema) - async def get_public_prolific_integration(self, language) -> PublicProlificIntegration: + async def get_public_prolific_integration(self, study_id, language) -> PublicProlificIntegration: applet_service = AppletService(self.session, uuid.UUID("00000000-0000-0000-0000-000000000000")) await applet_service.exist_by_key(self.applet_id) applet_base_info = await applet_service.get_info_by_key(self.applet_id, language) + self.applet_id = applet_base_info.id # Update the public applet key to be the real applet id - integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( - applet_id=applet_base_info.id, - integration_type=self.type - ) + api_key = await self._get_prolific_api_key() - return PublicProlificIntegration(enabled=integration is not None) + prolific_response = requests.get( + f"{PROLIFIC_API_BASE_URL}/studies/{study_id}/", + headers={ + **BASE_HEADERS, + "Authorization": f"Token {api_key}" + }) + + return PublicProlificIntegration(enabled=(api_key is not None and prolific_response.status_code == 200)) async def get_completion_codes(self, study_id: str) -> ProlificCompletionCodeList: - integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( - applet_id=self.applet_id, - integration_type=self.type - ) - - api_key = ast.literal_eval(integration.configuration)["api_key"] + api_key = await self._get_prolific_api_key() prolific_response = requests.get( - f"https://api.prolific.com/api/v1/studies/{study_id}/", + f"{PROLIFIC_API_BASE_URL}/studies/{study_id}/", headers={ - "Authorization": f"Token {api_key}", - "Content-Type": "application/json" - }) + **BASE_HEADERS, + "Authorization": f"Token {api_key}" + }) if (prolific_response.status_code != 200): raise HTTPException(status_code=prolific_response.status_code, detail=prolific_response.detail) - prolific_completion_codes = prolific_response.json()["completion_codes"] + return ProlificCompletionCodeList(completion_codes=prolific_response.json()["completion_codes"]) + + async def _get_prolific_api_key(self) -> str | None: + integration = await IntegrationsCRUD(self.session).retrieve_by_applet_and_type( + applet_id=self.applet_id, + integration_type=self.type + ) - completion_codes = [] - for code in prolific_completion_codes: - completion_codes.append(ProlificCompletionCode(code=code["code"], code_type=code["code_type"], actions=code["actions"], actor=code["actor"])) + if not integration: + return None - return ProlificCompletionCodeList(completion_codes=prolific_response.json()["completion_codes"]) + try: + return ProlificIntegration(**json.loads(integration.configuration)).api_key + except ValidationError: + raise HTTPException(status_code=400, detail="Prolific integration is not configured") diff --git a/src/apps/subjects/crud/subject.py b/src/apps/subjects/crud/subject.py index d16102d95b11..57971b667aa0 100644 --- a/src/apps/subjects/crud/subject.py +++ b/src/apps/subjects/crud/subject.py @@ -28,7 +28,6 @@ async def update(self, schema: SubjectSchema) -> SubjectSchema: return await self._update_one("id", schema.id, schema) async def update_by_id(self, id_, **values): - print(values) query = ( update(self.schema_class).where(self.schema_class.id == id_).values(**values).returning(self.schema_class) )