From 4f0a943fb16e9bb433d5919d2d35827ba9adf7b1 Mon Sep 17 00:00:00 2001 From: Brieuc Dubois <git@bhasher.com> Date: Thu, 12 Sep 2024 16:14:37 +0300 Subject: [PATCH] Implement weekly survey #49 --- backend/app/crud.py | 6 ++ backend/app/main.py | 21 +++++ backend/app/models.py | 13 +++ backend/app/schemas.py | 10 +++ frontend/src/lang/fr.json | 30 +++++++ frontend/src/lib/api/users.ts | 20 +++++ .../lib/components/users/weeklySurvey.svelte | 80 +++++++++++++++++++ frontend/src/lib/config.ts | 2 + frontend/src/lib/types/user.ts | 29 ++++++- frontend/src/routes/session/+page.svelte | 6 ++ 10 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/components/users/weeklySurvey.svelte diff --git a/backend/app/crud.py b/backend/app/crud.py index b24620c5..1183de4f 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -66,6 +66,12 @@ def create_contact(db: Session, user, contact): db.refresh(user) return user +def create_user_survey_weekly(db: Session, user_id: int, survey: schemas.SurveyCreate): + db_user_survey_weekly = models.UserSurveyWeekly(user_id=user_id, **survey.dict()) + db.add(db_user_survey_weekly) + db.commit() + db.refresh(db_user_survey_weekly) + return db_user_survey_weekly def get_contact_sessions(db: Session, user_id: int, contact_id: int): return ( diff --git a/backend/app/main.py b/backend/app/main.py index 20ef39f3..857b253d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -379,6 +379,27 @@ def get_contacts( return db_user.contacts + db_user.contact_of +@usersRouter.post("/{user_id}/surveys/weekly", status_code=status.HTTP_201_CREATED) +def create_weekly_survey( + user_id: int, + survey: schemas.UserSurveyWeeklyCreate, + db: Session = Depends(get_db), + current_user: schemas.User = Depends(get_jwt_user), +): + if ( + not check_user_level(current_user, models.UserType.ADMIN) + and current_user.id != user_id + ): + raise HTTPException( + status_code=401, + detail="You do not have permission to create a survey for this user", + ) + db_user = crud.get_user(db, user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + + return crud.create_user_survey_weekly(db, user_id, survey).id + @sessionsRouter.post("", response_model=schemas.Session) def create_session( db: Session = Depends(get_db), diff --git a/backend/app/models.py b/backend/app/models.py index c9849dc4..7a03108e 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -47,6 +47,7 @@ class User(Base): gender = Column(String, default=None) calcom_link = Column(String, default="") study_id = Column(Integer, ForeignKey("studies.id"), default=None) + last_survey = Column(DateTime, default=None) sessions = relationship( "Session", secondary="user_sessions", back_populates="users" @@ -69,6 +70,18 @@ class User(Base): ) +class UserSurveyWeekly(Base): + __tablename__ = "users_survey_weekly" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=datetime.datetime.now) + user_id = Column(Integer, ForeignKey("users.id")) + q1 = Column(Float) + q2 = Column(Float) + q3 = Column(Float) + q4 = Column(Float) + + class Session(Base): __tablename__ = "sessions" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 68d60de3..5e75ab72 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -18,6 +18,7 @@ class User(BaseModel): gender: str | None = None calcom_link: str | None study_id: int | None = None + last_survey: datetime.datetime | None = None class Config: from_attributes = True @@ -37,6 +38,7 @@ class UserCreate(BaseModel): gender: str | None = None calcom_link: str | None = None study_id: int | None = None + last_survey: datetime.datetime | None = None class UserUpdate(BaseModel): @@ -53,6 +55,7 @@ class UserUpdate(BaseModel): gender: str | None = None calcom_link: str | None = None study_id: int | None = None + last_survey: datetime.datetime | None = None class Config: from_attributes = True @@ -65,6 +68,13 @@ class ContactCreate(BaseModel): from_attributes = True +class UserSurveyWeeklyCreate(BaseModel): + q1: float + q2: float + q3: float + q4: float + + class Session(BaseModel): id: int created_at: datetime.datetime diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 682189b6..b6cf7887 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -211,6 +211,36 @@ "q1": "À quel point cette application est-elle utile ?", "q2": "À quel point cette application est-elle facile à utiliser ?", "q3": "Remarques éventuelles" + }, + "weekly": { + "title": "Questionnaire hebdomadaire", + "description": "Au cours des 7 derniers jours...", + "questions": [ + "Combien d'heures de <span class='font-bold'>cours</span> de {TARGET_LANGUAGE} avez vous suivies ?", + "Combien d'heures avez-vous <span class='font-bold'>regardé des vidéos</span> en {TARGET_LANGUAGE} (films, séries, Youtube...) ou <span class='font-bold'>écouté des contenus</span> en {TARGET_LANGUAGE} (podcasts, radio, cours universitaires...) ?", + "Combien d'heures avez-vous <span class='font-bold'>lu des textes</span> en {TARGET_LANGUAGE} (livre, journal, BD, sites web...) ?", + "Combien d'heures avez-vous <span class='font-bold'>parlé</span> en {TARGET_LANGUAGE} (discussions avec amis, famille, collègues...) ?" + ], + "answers": { + "placeholder": "", + "0": "Aucune", + "05": "30 minutes ou moins", + "1": "1 heure", + "2": "2 heures", + "3": "3 heures", + "4": "4 heures", + "5": "5 heures", + "6": "6 heures", + "7": "7 heures", + "8": "8 heures", + "9": "9 heures", + "10": "10 heures ou plus" + }, + "errors": { + "null": "Veuillez répondre à toutes les questions", + "submit": "Erreur lors de l'envoi du questionnaire" + }, + "success": "Questionnaire envoyé, merci !" } } }, diff --git a/frontend/src/lib/api/users.ts b/frontend/src/lib/api/users.ts index 61de74e9..b5d3ced3 100644 --- a/frontend/src/lib/api/users.ts +++ b/frontend/src/lib/api/users.ts @@ -145,3 +145,23 @@ export async function createTestTypingAPI( return response.data; } + +export async function createWeeklySurveyAPI( + user_id: number, + q1: number, + q2: number, + q3: number, + q4: number +): Promise<number | null> { + const response = await axiosInstance.post(`/users/${user_id}/surveys/weekly`, { + q1, + q2, + q3, + q4 + }); + if (response.status !== 201) { + toastAlert('Failed to create weekly survey'); + return null; + } + return response.data; +} diff --git a/frontend/src/lib/components/users/weeklySurvey.svelte b/frontend/src/lib/components/users/weeklySurvey.svelte new file mode 100644 index 00000000..a3f0fcbf --- /dev/null +++ b/frontend/src/lib/components/users/weeklySurvey.svelte @@ -0,0 +1,80 @@ +<script lang="ts"> + import { createWeeklySurveyAPI } from '$lib/api/users'; + import config from '$lib/config'; + import { t } from '$lib/services/i18n'; + import { user } from '$lib/types/user'; + import { toastAlert, toastSuccess, toastWarning } from '$lib/utils/toasts'; + + let open = + !$user?.last_survey || $user.last_survey.getTime() + config.WEEKLY_SURVEY_INTERVAL < Date.now(); + + async function send() { + if (!$user) return; + + const data = Array.from({ length: 4 }, (_, i) => { + const value = (document.getElementById('questions-' + i) as HTMLSelectElement).value; + return value === '-1' ? null : parseFloat(value); + }); + + if (data.includes(null)) { + toastWarning($t('session.modal.weekly.errors.null')); + return; + } + + const res = await createWeeklySurveyAPI($user.id, data[0]!, data[1]!, data[2]!, data[3]!); + + if (!res) { + toastAlert($t('session.modal.weekly.errors.submit')); + } + + await $user.patch({ last_survey: new Date() }); + + open = false; + + toastSuccess($t('session.modal.weekly.success')); + } +</script> + +<dialog + class="modal bg-black bg-opacity-50" + {open} + on:close={() => (open = false)} + on:keydown={(e) => e.key === 'Escape' && (open = false)} + tabindex="0" + aria-modal="true" +> + <div class="modal-box max-w-none"> + <h2 class="text-xl font-bold mb-4">{$t('session.modal.weekly.title')}</h2> + <p>{@html $t('session.modal.weekly.description')}</p> + {#each new Array(4) as _, i} + <label class="form-control w-full"> + <div class="label"> + <span class="label-text" + >{@html $t('session.modal.weekly.questions.' + i).replaceAll( + '{TARGET_LANGUAGE}', + $t('utils.language.' + $user?.target_language).toLowerCase() + )}</span + > + </div> + <select id={'questions-' + i} class="select select-bordered"> + <option value="-1" hidden selected + >{$t('session.modal.weekly.answers.placeholder')}</option + > + <option value="0">{$t('session.modal.weekly.answers.0')}</option> + <option value="0.5">{$t('session.modal.weekly.answers.05')}</option> + <option value="1">{$t('session.modal.weekly.answers.1')}</option> + <option value="2">{$t('session.modal.weekly.answers.2')}</option> + <option value="3">{$t('session.modal.weekly.answers.3')}</option> + <option value="4">{$t('session.modal.weekly.answers.4')}</option> + <option value="5">{$t('session.modal.weekly.answers.5')}</option> + <option value="6">{$t('session.modal.weekly.answers.6')}</option> + <option value="7">{$t('session.modal.weekly.answers.7')}</option> + <option value="8">{$t('session.modal.weekly.answers.8')}</option> + <option value="9">{$t('session.modal.weekly.answers.9')}</option> + <option value="10">{$t('session.modal.weekly.answers.10')}</option> + </select> + </label> + {/each} + <button class="btn btn-primary w-full mt-10" on:click={send}>{$t('button.submit')}</button> + </div> +</dialog> diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 86c8f761..10b3cf95 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -3,6 +3,8 @@ export default { API_PROXY: import.meta.env.VITE_API_PROXY || 'https://languagelab.sipr.ucl.ac.be:8000', APP_URL: import.meta.env.VITE_APP_URL || 'https://languagelab.sipr.ucl.ac.be', WS_URL: import.meta.env.VITE_WS_URL || 'wss://languagelab.sipr.ucl.ac.be/api/v1/ws', + // 1 week - 2 hours + WEEKLY_SURVEY_INTERVAL: (7 * 24 - 2) * 60 * 60 * 1000, LEARNING_LANGUAGES: { fra: 'French - fran\u00e7ais' }, diff --git a/frontend/src/lib/types/user.ts b/frontend/src/lib/types/user.ts index 692f92bf..ec8e2de3 100644 --- a/frontend/src/lib/types/user.ts +++ b/frontend/src/lib/types/user.ts @@ -1,4 +1,5 @@ import { createUserAPI, getUsersAPI, patchUserAPI } from '$lib/api/users'; +import { parseToLocalDate } from '$lib/utils/date'; import { toastAlert } from '$lib/utils/toasts'; import { get, writable } from 'svelte/store'; @@ -30,6 +31,8 @@ export default class User { private _birthdate: number | null; private _gender: string | null; private _calcom_link: string | null; + private _study_id: number | null; + private _last_survey: Date | null; private constructor( id: number, @@ -43,7 +46,9 @@ export default class User { target_language: string | null, birthdate: number | null, gender: string | null, - calcom_link: string | null + calcom_link: string | null, + study_id: number | null, + last_survey: Date | null ) { this._id = id; this._email = email; @@ -57,6 +62,8 @@ export default class User { this._birthdate = birthdate; this._gender = gender; this._calcom_link = calcom_link; + this._study_id = study_id; + this._last_survey = last_survey; } get id(): number { @@ -115,6 +122,14 @@ export default class User { return this._calcom_link; } + get study_id(): number | null { + return this._study_id; + } + + get last_survey(): Date | null { + return this._last_survey; + } + equals<T>(obj: T): boolean { if (obj === null || obj === undefined) return false; if (!(obj instanceof User)) return false; @@ -147,7 +162,9 @@ export default class User { target_language: this.target_language, birthdate: this.birthdate, gender: this.gender, - calcom_link: this.calcom_link + calcom_link: this.calcom_link, + study_id: this.study_id, + last_survey: this.last_survey }); } @@ -165,6 +182,8 @@ export default class User { if (data.birthdate) this._birthdate = data.birthdate; if (data.gender) this._gender = data.gender; if (data.calcum_link) this._calcom_link = data.calcom_link; + if (data.study_id) this._study_id = data.study_id; + if (data.last_survey) this._last_survey = data.last_survey; } return res; } @@ -195,6 +214,8 @@ export default class User { null, null, null, + null, + null, null ); users.add(user); @@ -235,7 +256,9 @@ export default class User { json.target_language, json.birthdate, json.gender, - json.calcom_link + json.calcom_link, + json.study_id, + json.last_survey === null ? null : new Date(json.last_survey) ); users.update((us) => { diff --git a/frontend/src/routes/session/+page.svelte b/frontend/src/routes/session/+page.svelte index cdc89480..9791e480 100644 --- a/frontend/src/routes/session/+page.svelte +++ b/frontend/src/routes/session/+page.svelte @@ -7,6 +7,8 @@ import { onMount } from 'svelte'; import { user } from '$lib/types/user'; import Gravatar from 'svelte-gravatar'; + import WeeklySurvey from '$lib/components/users/weeklySurvey.svelte'; + import config from '$lib/config.js'; export let data; let session: Session | null = null; @@ -61,3 +63,7 @@ <div class=""></div> </div> {/if} + +{#if $user} + <WeeklySurvey /> +{/if} -- GitLab