diff --git a/backend/alembic/versions/cef049467e32_study_info.py b/backend/alembic/versions/cef049467e32_study_info.py new file mode 100644 index 0000000000000000000000000000000000000000..76a972c9cce2d3c7f9b61606b4ed1db0dce3f228 --- /dev/null +++ b/backend/alembic/versions/cef049467e32_study_info.py @@ -0,0 +1,27 @@ +"""Study info + +Revision ID: cef049467e32 +Revises: c4ff1cfa66b7 +Create Date: 2025-03-13 00:19:21.655377 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "cef049467e32" +down_revision: Union[str, None] = "c4ff1cfa66b7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + op.drop_table("study_infos") diff --git a/backend/app/crud/studies.py b/backend/app/crud/studies.py index 5848676a762371bbc15532eaa4dfc1ceb4703b19..ce4923c55c3566c4a7ad3b2df2b4c87737ad012f 100644 --- a/backend/app/crud/studies.py +++ b/backend/app/crud/studies.py @@ -242,3 +242,15 @@ def download_study_wide(db: Session, study_id: int): "Content-Disposition": f"attachment; filename={study_id}-surveys-wide.csv" }, ) + + +def create_study_info( + db: Session, study_id: int, study_info: schemas.StudyInfoCreate +) -> models.StudyInfo: + db_study_info = models.StudyInfo( + study_id=study_id, **study_info.dict(exclude_unset=True) + ) + db.add(db_study_info) + db.commit() + db.refresh(db_study_info) + return db_study_info diff --git a/backend/app/models/studies.py b/backend/app/models/studies.py index 4882e1437ed31050376b6da97705dabf263eeca8..89df185a231487905bda032b0be2f5fb01880119 100644 --- a/backend/app/models/studies.py +++ b/backend/app/models/studies.py @@ -42,3 +42,15 @@ class StudyTest(Base): study_id = Column(Integer, ForeignKey("studies.id"), primary_key=True) test_id = Column(Integer, ForeignKey("tests.id"), primary_key=True) + + +class StudyInfo(Base): + __tablename__ = "study_infos" + id = Column(Integer, primary_key=True, index=True) + study_id = Column(Integer, ForeignKey("studies.id")) + rid = Column(String) + birthyear = Column(Integer) + gender = Column(String) + primary_language = Column(String) + other_languages = Column(String) + education = Column(String) diff --git a/backend/app/routes/studies.py b/backend/app/routes/studies.py index 04411b25ef7a1a6850a6fb65d0efc85379a62fac..d4ce1f466dca0b03771aa8b917440fd2f46b9fc8 100644 --- a/backend/app/routes/studies.py +++ b/backend/app/routes/studies.py @@ -75,7 +75,7 @@ def download_study( @require_admin("You do not have permission to download this study.") @studiesRouter.get("/{study_id}/download/surveys-wide") -def download_study( +def download_study_wide( study_id: int, db: Session = Depends(get_db), ): @@ -83,3 +83,12 @@ def download_study( if study is None: raise HTTPException(status_code=404, detail="Study not found") return crud.download_study_wide(db, study_id) + + +@studiesRouter.post("/{study_id}/info", status_code=status.HTTP_201_CREATED) +def create_study_info( + study_id: int, + study_info: schemas.StudyInfoCreate, + db: Session = Depends(get_db), +): + return crud.create_study_info(db, study_id, study_info) diff --git a/backend/app/schemas/studies.py b/backend/app/schemas/studies.py index 6dbeac99f9d72c5711d8919986976ea67348521c..9fbe529fd437df088e9bd56af0fd90feda61caaa 100644 --- a/backend/app/schemas/studies.py +++ b/backend/app/schemas/studies.py @@ -39,3 +39,12 @@ class Study(BaseModel): users: list[User] = [] tests: list[Test] = [] + + +class StudyInfoCreate(BaseModel): + rid: str + birthyear: int + gender: str + primary_language: str + other_languages: str + education: str diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 4b2ad4ce880860ae93e5c9d58b230caddf0d49c5..8136121e3ac9a7486fa2913320c466af4a30731c 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -386,6 +386,7 @@ "study": "Étude", "code": "Code", "tests": "Tests", + "infos": "Informations", "end": "Fin" }, "complete": "Merci pour votre participation !", diff --git a/frontend/src/lib/api/studies.ts b/frontend/src/lib/api/studies.ts index 8d116735240085b24236b33e788c7db7cfb4f8ea..f69eeb1430a2459d0b798f6b394fe0b555311e53 100644 --- a/frontend/src/lib/api/studies.ts +++ b/frontend/src/lib/api/studies.ts @@ -111,3 +111,29 @@ export async function createTestTypingAPI( return parseInt(await response.text()); } + +export async function sendStudyResponseInfoAPI( + fetch: fetchType, + survey_id: number, + rid: string, + birthyear: number, + gender: string, + primary_language: string, + other_languages: string, + education: string +) { + const response = await fetch(`/api/studies/${survey_id}/info`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + rid, + birthyear, + gender, + primary_language, + other_languages, + education + }) + }); + + return response.ok; +} diff --git a/frontend/src/lib/api/survey.ts b/frontend/src/lib/api/survey.ts index cebdca2986c9f9ceaa59e571820896aec0a555ad..f600aacb96b48d4c6371a4054fa12b40d2813053 100644 --- a/frontend/src/lib/api/survey.ts +++ b/frontend/src/lib/api/survey.ts @@ -50,29 +50,3 @@ export async function getSurveyScoreAPI(fetch: fetchType, survey_id: number, sid return await response.json(); } - -export async function sendSurveyResponseInfoAPI( - fetch: fetchType, - survey_id: number, - sid: string, - birthyear: number, - gender: string, - primary_language: string, - other_language: string, - education: string -) { - const response = await fetch(`/api/tests/info/${survey_id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - sid, - birthyear, - gender, - primary_language, - other_language, - education - }) - }); - - return response.ok; -} diff --git a/frontend/src/lib/components/surveys/endQuestions.svelte b/frontend/src/lib/components/surveys/endQuestions.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b3084a262d7a5e2ecf724c3994979901ef61a8b6 --- /dev/null +++ b/frontend/src/lib/components/surveys/endQuestions.svelte @@ -0,0 +1,145 @@ +<script lang="ts"> + import { sendStudyResponseInfoAPI } from '$lib/api/studies'; + import config from '$lib/config'; + import { t } from '$lib/services/i18n'; + import { toastAlert } from '$lib/utils/toasts'; + import Dropdown from './dropdown.svelte'; + + let { + study_id, + rid, + onFinish = () => {} + }: { study_id: number; rid: string; onFinish: Function } = $props(); + + let step = $state(0); + + const genderOptions = [ + { value: 'male', label: $t('surveys.genders.male') }, + { value: 'female', label: $t('surveys.genders.female') }, + { value: 'other', label: $t('surveys.genders.other') }, + { value: 'na', label: $t('surveys.genders.na') } + ]; + + let birthYear = ''; + let gender = ''; + let primaryLanguage = ''; + let other_language = ''; + let education = ''; + + let selectedOption: any = $state(); + + async function send() { + if ( + !(await sendStudyResponseInfoAPI( + fetch, + study_id, + rid, + parseInt(birthYear), + gender, + primaryLanguage, + other_language, + education + )) + ) { + toastAlert($t('surveys.info.error')); + } else { + onFinish(); + } + } +</script> + +{#if step === 0} + <div class="mx-auto mt-16 text-center px-4"> + <p class="text-center font-bold py-4 px-6 m-auto">{$t('surveys.birthYear')}</p> + <Dropdown + values={Array.from({ length: 82 }, (_, i) => { + const year = 1931 + i; + return { value: year, display: year }; + }).reverse()} + bind:option={selectedOption} + placeholder={$t('surveys.birthYear')} + funct={() => { + birthYear = selectedOption; + step++; + }} + ></Dropdown> + </div> +{:else if step === 1} + <div class="mx-auto mt-16 text-center px-4"> + <p class="text-center font-bold py-4 px-6 m-auto">{$t('surveys.gender')}</p> + <div class="flex flex-col items-center space-y-4"> + {#each genderOptions as { value, label }} + <label class="radio-label flex items-center space-x-2"> + <input + type="radio" + name="gender" + {value} + onchange={() => { + gender = value; + step++; + }} + required + class="radio-button" + /> + <span>{label}</span> + </label> + {/each} + </div> + </div> +{:else if step === 2} + <div class="mx-auto mt-16 text-center px-4"> + <p class="text-center font-bold py-4 px-6 m-auto">{$t('surveys.homeLanguage')}</p> + <Dropdown + values={Object.entries(config.PRIMARY_LANGUAGE).map(([code, name]) => ({ + value: code, + display: name + }))} + bind:option={selectedOption} + placeholder={$t('surveys.homeLanguage')} + funct={() => { + primaryLanguage = selectedOption; + step++; + }} + ></Dropdown> + </div> +{:else if step === 3} + <div class="mx-auto mt-16 text-center px-4"> + <p class="text-center font-bold py-4 px-6 m-auto">{$t('surveys.otherLanguage')}</p> + <p class="mb-6 text-sm text-gray-600 text-center">{$t('surveys.otherLanguageNote')}</p> + <Dropdown + values={[ + { value: 'none', display: '/' }, + ...Object.entries(config.PRIMARY_LANGUAGE).map(([code, name]) => ({ + value: code, + display: name + })) + ]} + bind:option={selectedOption} + placeholder={$t('surveys.otherLanguage')} + funct={() => { + other_language = selectedOption; + step++; + }} + ></Dropdown> + </div> +{:else if step === 4} + <div class="mx-auto mt-16 text-center px-4"> + <p class="text-center font-bold py-4 px-6 m-auto">{$t('surveys.education.title')}</p> + <Dropdown + values={[ + { value: 'NoEducation', display: $t('surveys.education.NoEducation') }, + { value: 'PrimarySchool', display: $t('surveys.education.PrimarySchool') }, + { value: 'SecondarySchool', display: $t('surveys.education.SecondarySchool') }, + { value: 'NonUni', display: $t('surveys.education.NonUni') }, + { value: 'Bachelor', display: $t('surveys.education.Bachelor') }, + { value: 'Master', display: $t('surveys.education.Master') } + ]} + bind:option={selectedOption} + placeholder={$t('surveys.education.title')} + funct={() => { + education = selectedOption; + send(); + }} + ></Dropdown> + </div> +{/if} diff --git a/frontend/src/routes/studies/[[id]]/+page.svelte b/frontend/src/routes/studies/[[id]]/+page.svelte index 81ae73c2e4e51fbb8b6f2ee807f44f09f0aa85de..358954930a8a5e2eebebdc4906cf7e764e181d88 100644 --- a/frontend/src/routes/studies/[[id]]/+page.svelte +++ b/frontend/src/routes/studies/[[id]]/+page.svelte @@ -9,6 +9,7 @@ import { TestTask, TestTyping } from '$lib/types/tests'; import Typingbox from '$lib/components/tests/typingbox.svelte'; import { getTestEntriesScoreAPI } from '$lib/api/tests'; + import EndQuestions from '$lib/components/surveys/endQuestions.svelte'; let { data, form }: { data: PageData; form: FormData } = $props(); let study: Study | undefined = $state(data.study); @@ -63,6 +64,9 @@ </li> {/if} <li class="step" class:step-primary={study && current_step >= study.tests.length + 2}> + {$t('studies.tab.infos')} + </li> + <li class="step" class:step-primary={study && current_step >= study.tests.length + 3}> {$t('studies.tab.end')} </li> </ul> @@ -158,6 +162,10 @@ {/if} {/key} {:else if current_step == study.tests.length + 2} + <div class="flex flex-col h-full"> + <EndQuestions study_id={study.id} {rid} onFinish={() => current_step++} /> + </div> + {:else if current_step == study.tests.length + 3} <div class="flex flex-col h-full"> <div class="flex-grow text-center mt-16"> <span>{$t('studies.complete')}</span>