diff --git a/backend/alembic.ini b/backend/alembic.ini index 8a8c59f924cea46a0d23fb1ba72e18bc0f6a39d0..0f2f4ecbb9b50aa2eab839853d3e91b4827675b9 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -10,6 +10,9 @@ sqlalchemy.url = sqlite:///languagelab.sqlite [post_write_hooks] hooks = black +black.type = console_scripts +black.entrypoint = black +black.options = -l 79 [loggers] keys = root,sqlalchemy,alembic diff --git a/backend/alembic/versions/fe09c6f768cd_add_code_column_to_test_typing_table.py b/backend/alembic/versions/fe09c6f768cd_add_code_column_to_test_typing_table.py new file mode 100644 index 0000000000000000000000000000000000000000..1b99fa42d1631885030d72a3ac7a0d2d342d4057 --- /dev/null +++ b/backend/alembic/versions/fe09c6f768cd_add_code_column_to_test_typing_table.py @@ -0,0 +1,33 @@ +"""Add code column to test_typing table + +Revision ID: fe09c6f768cd +Revises: 9038306d44fc +Create Date: 2025-01-31 21:45:56.343739 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "fe09c6f768cd" +down_revision: Union[str, None] = "0bf670c4a564" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + op.create_table( + "_tmp_test_typing", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + op.drop_table("test_typing") + op.rename_table("_tmp_test_typing", "test_typing") diff --git a/backend/app/crud.py b/backend/app/crud.py index 41be44b063f73793b49614238cbc472fe20a0131..3cfb20fde9fe30249a03ecd47cdafcfe02875406 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -263,8 +263,8 @@ def delete_message_feedback(db: Session, feedback_id: int): db.commit() -def create_test_typing(db: Session, test: schemas.TestTypingCreate, user: schemas.User): - db_test = models.TestTyping(user_id=user.id) +def create_test_typing(db: Session, test: schemas.TestTypingCreate): + db_test = models.TestTyping(code=test.code) db.add(db_test) db.commit() db.refresh(db_test) diff --git a/backend/app/main.py b/backend/app/main.py index 7acb7331954b18e356241afde5f97de9b07162ee..d1a4cc36d5b041fcac04a0dba490560bf9448ead 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -290,28 +290,14 @@ def store_typing_entries( pass -@usersRouter.post("/{user_id}/tests/typing", status_code=status.HTTP_201_CREATED) +@studyRouter.post("/typing", status_code=status.HTTP_201_CREATED) def create_test_typing( - user_id: int, test: schemas.TestTypingCreate, background_tasks: BackgroundTasks, 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 test 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") - typing_id = crud.create_test_typing(db, test, db_user).id + typing_id = crud.create_test_typing(db, test).id if test.entries: background_tasks.add_task(store_typing_entries, db, test.entries, typing_id) diff --git a/backend/app/models.py b/backend/app/models.py index b156ea33e33e5a983440aeab2f26f8a865fbab29..70bf39b6363f5d9033dc2c80977ccd30d76cb162 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -181,8 +181,8 @@ class TestTyping(Base): __tablename__ = "test_typing" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), index=True) created_at = Column(DateTime, default=datetime_aware) + code = Column(String) entries = relationship("TestTypingEntry", backref="typing") diff --git a/backend/app/schemas.py b/backend/app/schemas.py index a5b75b334374db6498d645842a546531c911f016..bfd43f915117952ae4310b8ba802e27c7574bb1d 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -225,6 +225,7 @@ class TestTypingEntryCreate(BaseModel): class TestTypingCreate(BaseModel): entries: list[TestTypingEntryCreate] + code: str class TestVocabularyCreate(BaseModel): diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json index 3d5a33f8cfcceb356c640b7ac3de589bd608b6b1..dc0f0b273ceba7e2583c41c8001053d877fa7b46 100644 --- a/frontend/src/lang/fr.json +++ b/frontend/src/lang/fr.json @@ -238,7 +238,8 @@ }, "tests": { "sendResults": "Envoyer", - "sendResultsDone": "Envoyé" + "sendResultsDone": "Envoyé", + "typing": "Test de frappe" }, "surveys": { "introduction": "Ceci est un questionnaire.", diff --git a/frontend/src/lib/api/studies.ts b/frontend/src/lib/api/studies.ts index 6616c897a752c2b4cafc35d5a272bdeff9303e08..7eff0b9115e82fc9001f425aa743c6244664d056 100644 --- a/frontend/src/lib/api/studies.ts +++ b/frontend/src/lib/api/studies.ts @@ -77,3 +77,19 @@ export async function removeUserToStudyAPI( }); return response.ok; } + +export async function createTestTypingAPI( + fetch: fetchType, + entries: typingEntry[], + code: string +): Promise<number | null> { + const response = await fetch(`/api/studies/typing`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entries, code }) + }); + + if (!response.ok) return null; + + return parseInt(await response.text()); +} diff --git a/frontend/src/lib/api/tests.ts b/frontend/src/lib/api/tests.ts index f0019c47f651f4beaa74893563d25b623fbc55e4..f8c69cd708513399375e482981efbff69743cbcd 100644 --- a/frontend/src/lib/api/tests.ts +++ b/frontend/src/lib/api/tests.ts @@ -1,3 +1,5 @@ +import type { fetchType } from '$lib/utils/types'; + export async function sendTestVocabularyAPI(data: any): Promise<boolean> { const response = await fetch(`/api/tests/vocabulary`, { method: 'POST', diff --git a/frontend/src/lib/api/users.ts b/frontend/src/lib/api/users.ts index 300bdb0beb03c98701ed6952a8bdb9ea974166c7..8e64df18e8a150ff7fe54e5eb16f0c2606eda531 100644 --- a/frontend/src/lib/api/users.ts +++ b/frontend/src/lib/api/users.ts @@ -87,22 +87,6 @@ export async function patchUserAPI(fetch: fetchType, user_id: number, data: any) return response.ok; } -export async function createTestTypingAPI( - fetch: fetchType, - user_id: number, - entries: typingEntry[] -): Promise<number | null> { - const response = await fetch(`/api/users/${user_id}/tests/typing`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ entries }) - }); - - if (!response.ok) return null; - - return parseInt(await response.text()); -} - export async function createWeeklySurveyAPI( fetch: fetchType, user_id: number, diff --git a/frontend/src/lib/components/tests/typingtest.svelte b/frontend/src/lib/components/tests/typingtest.svelte index 103aba2f0d8976d05d740723a9051a73392d2d58..931cfdc7b9ea47d4ce550793614a027bac48bf59 100644 --- a/frontend/src/lib/components/tests/typingtest.svelte +++ b/frontend/src/lib/components/tests/typingtest.svelte @@ -1,79 +1,122 @@ <script lang="ts"> import { t } from '$lib/services/i18n'; import Typingbox from '$lib/components/tests/typingbox.svelte'; - import { get } from 'svelte/store'; - import { createTestTypingAPI } from '$lib/api/users'; import type User from '$lib/types/user'; + import { toastWarning } from '$lib/utils/toasts'; + import { get } from 'svelte/store'; + import { createTestTypingAPI } from '$lib/api/studies'; - let { user, onFinish }: { user: User; onFinish: Function } = $props(); + let { user, onFinish }: { user: User | null; onFinish: Function } = $props(); let data: typingEntry[] = $state([]); - let currentExercice = $state(0); - let inProgress = $state(false); let exercices = [ { - duration: 15, - explications: `Repetez les lettres "dk" autant de fois que possible en 15 secondes. Le chronomètre démarre dès que vous appuyez sur une touche ou sur le boutton ${get(t)('button.start')}. Une vois que vous aurez terminé, appuyez sur le bouton ${get(t)('button.next')} pour passer à l'exercice suivant.`, - text: 'dk'.repeat(150) + '...' + duration: 30, + explications: 'Repetez la phrase suivante autant de fois que possible en 30 secondes.', + text: 'une femme folle tenait un verre dans sa main\n'.repeat(20) + '...' }, { duration: 30, explications: 'Repetez la phrase suivante autant de fois que possible en 30 secondes.', - text: 'Le chat est sur le toit.\n'.repeat(20) + '...' + text: 'the cat was sleeping under the apple tree\n'.repeat(20) + '...' }, { duration: -1, explications: 'Repetez 7 fois la phrase suivante le plus rapidement possible.', - text: 'Six animaux mangent\n'.repeat(6) + 'Six animaux mangent' + text: + 'trois heures raisonnables\nhuit histoires profondes\ndeux besoins fantastiques\nsix bijoux bizarres\n'.repeat( + 6 + ) + + 'trois heures raisonnables\nhuit histoires profondes\ndeux besoins fantastiques\nsix bijoux bizarres' + }, + { + duration: -1, + explications: 'Repetez 7 fois la phrase suivante le plus rapidement possible.', + text: + 'four interesting questions\nseven wonderful surprises\nfive important behaviours\nsome awkward zigzags\n'.repeat( + 6 + ) + + 'four interesting questions\nseven wonderful surprises\nfive important behaviours\nsome awkward zigzags' + }, + { + duration: -1, + explications: 'Écrivez aussi vite que possible la suite suivante:', + text: 'some awkward zigzags' } ]; async function submit() { - const res = await createTestTypingAPI(fetch, user.id, data); + if (!code) return; + const res = await createTestTypingAPI(fetch, data, code); if (!res) return; onFinish(); } + + let step = $state(user ? 1 : 0); + let code: string | undefined = $state(user ? user.email : undefined); + + function checkCode() { + if (!code) { + toastWarning(get(t)('surveys.invalidCode')); + return; + } + if (code.length < 3) { + toastWarning(get(t)('surveys.invalidCode')); + return; + } + + step += 1; + } </script> -{#each exercices as _, i (i)} - {#if i === currentExercice} - <Typingbox - exerciceId={i} - initialDuration={exercices[i].duration} - explications={exercices[i].explications} - text={exercices[i].text} - bind:data - bind:inProgress - onFinish={() => { - inProgress = false; - setTimeout(() => { - currentExercice++; - }, 3000); - }} +{#if step === 0} + <div class="max-w-screen-md mx-auto p-20 flex flex-col items-center min-h-screen"> + <h2 class="mb-10 text-xl text-center">{$t('tests.typing')}</h2> + <p class="mb-4 text-lg font-semibold">{$t('surveys.code')}</p> + <p class="mb-6 text-sm text-gray-600 text-center">{@html $t('surveys.codeIndication')}</p> + <input + type="text" + placeholder="Code" + class="input block mx-auto w-full max-w-xs border border-gray-300 rounded-md py-2 px-3 text-center" + onkeydown={(e) => e.key === 'Enter' && checkCode()} + bind:value={code} /> - {/if} -{/each} - -<div class="flex items-center mt-8"> - {#if currentExercice < exercices.length - 1} <button - class="button m-auto" - onclick={() => { - currentExercice++; - inProgress = false; - }} - disabled={inProgress} + class="button mt-4 block bg-yellow-500 text-white rounded-md py-2 px-6 hover:bg-yellow-600 transition" + onclick={checkCode} > {$t('button.next')} </button> - {:else} - <button class="button m-auto" disabled={inProgress} onclick={submit} - >{$t('button.submit')}</button - > - {/if} -</div> + </div> +{:else if step <= exercices.length} + {@const j = step - 1} + {#each exercices as _, i (i)} + {#if i === j} + <Typingbox + exerciceId={i} + initialDuration={exercices[i].duration} + explications={exercices[i].explications} + text={exercices[i].text} + bind:data + bind:inProgress + onFinish={() => { + inProgress = false; + setTimeout(() => { + step++; + }, 3000); + }} + /> + {/if} + {/each} +{:else} + <div class="flex items-center mt-8"> + <button class="button m-auto" disabled={inProgress} onclick={submit}> + {$t('button.submit')} + </button> + </div> +{/if} diff --git a/frontend/src/routes/tests/[id]/+page.svelte b/frontend/src/routes/tests/[id]/+page.svelte index 6e37703e2f634cf7e477b67714f86ca120119a00..a2c19dcb368bc36afa58d6a514b79c59d47b8970 100644 --- a/frontend/src/routes/tests/[id]/+page.svelte +++ b/frontend/src/routes/tests/[id]/+page.svelte @@ -25,14 +25,12 @@ } let step = $state(user ? 2 : 0); - let uuid = $state(user?.email || ''); let uid = $state(user?.id || null); let code = $state(''); let subStep = $state(0); let currentGroupId = $state(0); survey.groups.sort((a, b) => { - //puts the demo questions first if (a.demo === b.demo) { return 0; } diff --git a/frontend/src/routes/tests/typing/+page.server.ts b/frontend/src/routes/tests/typing/+page.server.ts index 8bdcc4ffa4d7daa8a62ce92a8bfcb493f5010a65..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/frontend/src/routes/tests/typing/+page.server.ts +++ b/frontend/src/routes/tests/typing/+page.server.ts @@ -1,7 +0,0 @@ -import { redirect, type ServerLoad } from '@sveltejs/kit'; - -export const load: ServerLoad = async ({ params, locals }) => { - if (locals.user == null || locals.user == undefined) { - redirect(303, '/login?redirect=/tests/typing/' + params.id); - } -}; diff --git a/frontend/src/routes/tests/typing/+page.svelte b/frontend/src/routes/tests/typing/+page.svelte index ad66eb237c3ffa9eacca42b7f8410255af5be9c0..17ec3c9122b225c5bc9fa3a475bc55fd3e1b2648 100644 --- a/frontend/src/routes/tests/typing/+page.svelte +++ b/frontend/src/routes/tests/typing/+page.svelte @@ -5,7 +5,7 @@ let finished = $state(false); let { data } = $props(); - let user = data.user!; + let user = data.user; </script> {#if finished} diff --git a/scripts/surveys/survey_maker.ipynb b/scripts/surveys/survey_maker.ipynb deleted file mode 100644 index b03ead5ab9517b9dc2647a5237d4fd19b9b4f73a..0000000000000000000000000000000000000000 --- a/scripts/surveys/survey_maker.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "5f682366-5ab6-4418-84b9-b971266af67f", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import requests\n", - "import numpy as np\n", - "import os" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e8af376d-ce09-4537-a724-2028777adef3", - "metadata": {}, - "outputs": [], - "source": [ - "API_URL = 'http://127.0.0.1:8000'\n", - "LOCAL_ITEMS_FOLDER = '../../frontend/static/surveys/items'\n", - "REMOTE_ITEMS_FOLDER = '/surveys/items'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30116c8a-a0a6-4dc7-9b69-8afb50a6252e", - "metadata": {}, - "outputs": [], - "source": [ - "df_items = pd.read_csv('items.csv',dtype = str)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "265ccfaa-e86d-460c-be36-1bdfbd2c433f", - "metadata": {}, - "outputs": [], - "source": [ - "df_items.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "93beb77a-7e30-437d-9f7e-e079cd2db029", - "metadata": {}, - "outputs": [], - "source": [ - "items = []\n", - "for i, row in df_items.iterrows():\n", - " row = row.dropna()\n", - " id_ = int(row['id'])\n", - "\n", - " o = {'id': id_, 'question': None, 'correct': None}\n", - " items.append(o)\n", - " \n", - " if 'question' in row:\n", - " o['question'] = f'text:{row[\"question\"]}'\n", - " elif os.path.isfile(f'{LOCAL_ITEMS_FOLDER}/{id_}/q.mp3'):\n", - " o['question'] = f'audio:{REMOTE_ITEMS_FOLDER}/{id_}/q.mp3'\n", - " elif os.path.isfile(f'{LOCAL_ITEMS_FOLDER}/{id_}/q.jpeg'):\n", - " o['question'] = f'image:{REMOTE_ITEMS_FOLDER}/{id_}/q.jpeg'\n", - " else:\n", - " print(f'Failed to find a question for item {id_}')\n", - "\n", - " if 'correct' in row:\n", - " o['correct'] = int(row['correct'])\n", - " else:\n", - " print(f'Failed to find corect for item {id_}')\n", - "\n", - " for j in range(1,9):\n", - " op = f'option{j}'\n", - " if op in row:\n", - " o[op] = 'text:' + row[op]\n", - " elif os.path.isfile(f'{LOCAL_ITEMS_FOLDER}/{id_}/{j}.mp3'):\n", - " o[op] = f'audio:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.mp3'\n", - " elif os.path.isfile(f'{LOCAL_ITEMS_FOLDER}/{id_}/{j}.jpeg'):\n", - " o[op] = f'image:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.jpeg'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a2bc630b-af5a-4f65-b6f6-4005d2f359f5", - "metadata": {}, - "outputs": [], - "source": [ - "groups = []\n", - "with open('groups.csv') as file:\n", - " file.readline()\n", - " for line in file.read().split('\\n'):\n", - " if not line:\n", - " continue\n", - " id_, title, *its = line.split(',')\n", - " id_ = int(id_)\n", - " its = [int(x) for x in its]\n", - " groups.append({'id': id_, 'title': title, 'items_id': its})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d8fc0ea-6ec9-4c58-8395-2a415d556fea", - "metadata": {}, - "outputs": [], - "source": [ - "surveys = []\n", - "with open('surveys.csv') as file:\n", - " file.readline()\n", - " for line in file.read().split('\\n'):\n", - " if not line:\n", - " continue\n", - " id_, title, *gps = line.split(',')\n", - " id_ = int(id_)\n", - " gps = [int(x) for x in gps]\n", - " surveys.append({'id': id_, 'title': title, 'groups_id': gps})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "39c018e7-2fb8-4b39-bd9b-fbffb810a823", - "metadata": {}, - "outputs": [], - "source": [ - "username = input('Username: ')\n", - "password = input('Password: ')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96102efa-019b-4ee3-8c58-02de14b16e0c", - "metadata": {}, - "outputs": [], - "source": [ - "session = requests.session()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6eca54d9-ae9a-42c2-a825-33164c0b1591", - "metadata": {}, - "outputs": [], - "source": [ - "assert session.post(API_URL + '/api/v1/auth/login', data={'email': username, 'password': password}).status_code == 200, 'Wrong username or password'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b001af43-1f12-4994-8403-5dd4a165879e", - "metadata": {}, - "outputs": [], - "source": [ - "for item in items:\n", - " assert session.delete(f'{API_URL}/api/v1/surveys/items/{item[\"id\"]}').status_code in [404, 204], f'Failed to delete item {item[\"id\"]}'\n", - " r = session.post(f'{API_URL}/api/v1/surveys/items', json=item)\n", - " if r.status_code not in [201]:\n", - " print(f'Failed to create item {item[\"id\"]}: {r.text}')\n", - " break\n", - "else:\n", - " print(f'Successfully created {len(items)} items')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31e2bd80-5569-407b-a562-5140d016093e", - "metadata": {}, - "outputs": [], - "source": [ - "for group in groups:\n", - " group = group.copy()\n", - " its = group.pop('items_id')\n", - " assert session.delete(f'{API_URL}/api/v1/surveys/groups/{group[\"id\"]}').status_code in [404, 204], f'Failed to delete group {group[\"id\"]}'\n", - " r = session.post(f'{API_URL}/api/v1/surveys/groups', json=group)\n", - " if r.status_code not in [201]:\n", - " print(f'Failed to create group {group[\"id\"]}: {r.text}')\n", - " break\n", - "\n", - " for it in its:\n", - " assert session.delete(f'{API_URL}/api/v1/surveys/groups/{group[\"id\"]}/items/{it}').status_code in [404, 204], f'Failed to delete item {it} from group {group[\"id\"]}'\n", - " r = session.post(f'{API_URL}/api/v1/surveys/groups/{group[\"id\"]}/items', json={\"question_id\": it})\n", - " if r.status_code not in [201]:\n", - " print(f'Failed to add item {it} to group {group[\"id\"]}: {r.text}')\n", - " break\n", - "\n", - "else:\n", - " print(f'Successfully created {len(groups)} groups')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3520668b-04fb-48f1-acbf-510ff4bc18c4", - "metadata": {}, - "outputs": [], - "source": [ - "for survey in surveys:\n", - " survey = survey.copy()\n", - " gps = survey.pop('groups_id')\n", - " assert session.delete(f'{API_URL}/api/v1/surveys/{survey[\"id\"]}').status_code in [404, 204], f'Failed to delete survey {survey[\"id\"]}'\n", - " r = session.post(f'{API_URL}/api/v1/surveys', json=survey)\n", - " if r.status_code not in [201]:\n", - " print(f'Failed to create suvey {survey[\"id\"]}: {r.text}')\n", - " break\n", - "\n", - " for gp in gps:\n", - " assert session.delete(f'{API_URL}/api/v1/surveys/{survey[\"id\"]}/groups/{gp}').status_code in [404, 204], f'Failed to delete gp {gp} from survey {survey[\"id\"]}'\n", - " r = session.post(f'{API_URL}/api/v1/surveys/{survey[\"id\"]}/groups', json={\"group_id\": gp})\n", - " if r.status_code not in [201]:\n", - " print(f'Failed to add group {gp} to survey {survey[\"id\"]}: {r.text}')\n", - " break\n", - "\n", - "else:\n", - " print(f'Successfully created {len(groups)} surveys')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -}