diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 39ee3f3a5dfc0b2222c1f86429a3f770339027c4..0000000000000000000000000000000000000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/backend/alembic/versions/344d94d32fa1_new_studies_tests_logic.py b/backend/alembic/versions/344d94d32fa1_new_studies_tests_logic.py
new file mode 100644
index 0000000000000000000000000000000000000000..be65438a70ece629d7da77c75aeabe76a4bb5ec8
--- /dev/null
+++ b/backend/alembic/versions/344d94d32fa1_new_studies_tests_logic.py
@@ -0,0 +1,129 @@
+"""New studies & tests logic
+
+Revision ID: 344d94d32fa1
+Revises: 9dfb49268c5f
+Create Date: 2025-03-07 15:43:38.917230
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision: str = "344d94d32fa1"
+down_revision: Union[str, None] = "9dfb49268c5f"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    with op.batch_alter_table("studies") as batch_op:
+        batch_op.drop_column("chat_duration")
+        batch_op.add_column(
+            sa.Column("nb_session", sa.Integer, nullable=False, server_default="8")
+        )
+        batch_op.add_column(
+            sa.Column(
+                "consent_participation",
+                sa.String,
+                nullable=False,
+                server_default="Si vous acceptez de participer, vous devrez seulement répondre à ce questionnaire et éventuellement à un autre test, si indiqué par la personne qui administre le test.",
+            )
+        )
+        batch_op.add_column(
+            sa.Column(
+                "consent_privacy",
+                sa.String,
+                nullable=False,
+                server_default="Les données collectées (vos réponses aux différentes questions) seront traitées de manière confidentielle et anonyme. Elles seront conservées après leur anonymisation intégrale et ne pourront être utilisées qu'à des fins scientifiques ou pédagogiques. Elles pourront éventuellement être partagées avec d'autres chercheurs ou enseignants, mais toujours dans ce cadre strictement de recherche ou d'enseignement.",
+            )
+        )
+        batch_op.add_column(
+            sa.Column(
+                "consent_rights",
+                sa.String,
+                nullable=False,
+                server_default="Votre participation à cette étude est volontaire. Vous pouvez à tout moment décider de ne plus participer à l'étude sans avoir à vous justifier. Vous pouvez également demander à ce que vos données soient supprimées à tout moment. Si vous avez des questions ou des préoccupations concernant cette étude, vous pouvez contacter le responsable de l'étude, Serge Bibauw, à l'adresse suivante :",
+            )
+        )
+        batch_op.add_column(
+            sa.Column(
+                "consent_study_data", sa.String, nullable=False, server_default=""
+            )
+        )
+
+    with op.batch_alter_table("studies") as batch_op:
+        batch_op.alter_column("nb_session", server_default=None)
+        batch_op.alter_column("consent_participation", server_default=None)
+        batch_op.alter_column("consent_privacy", server_default=None)
+        batch_op.alter_column("consent_rights", server_default=None)
+        batch_op.alter_column("consent_study_data", server_default=None)
+
+    op.drop_table("study_surveys")
+    op.drop_table("survey_group_questions")
+    op.drop_table("survey_groups")
+    op.drop_table("survey_questions")
+    op.drop_table("survey_response_info")  ## DATA LOSS
+    op.drop_table("survey_responses")  ## DATA LOSS
+    op.drop_table("survey_survey_groups")
+    op.drop_table("survey_surveys")
+    op.drop_table("test_typing")
+    op.drop_table("test_typing_entry")  ## DATA LOSS
+
+    # Auto-generated tables:
+    #   study_tests
+    #   test_entries
+    #   test_entries_task
+    #   test_entries_task_gapfill
+    #   test_entries_task_qcm
+    #   test_entries_typing
+    #   test_task_group_questions
+    #   test_task_groups
+    #   test_task_questions
+    #   test_task_questions_qcm
+    #   test_task_task_groups
+    #   test_tasks
+    #   tests
+
+
+def downgrade() -> None:
+    with op.batch_alter_table("studies") as batch_op:
+        batch_op.add_column(
+            sa.Column(
+                "chat_duration", sa.Integer, nullable=False, server_default="3600"
+            )
+        )
+        batch_op.drop_column("nb_session")
+        batch_op.drop_column("consent_participation")
+        batch_op.drop_column("consent_privacy")
+        batch_op.drop_column("consent_rights")
+        batch_op.drop_column("consent_study_data")
+
+    op.alter_column("studies", "chat_duration", server_default=None)
+
+    op.drop_table("study_tests")
+    op.drop_table("test_entries")  # DATA LOSS
+    op.drop_table("test_entries_task")  # DATA LOSS
+    op.drop_table("test_entries_task_gapfill")  # DATA LOSS
+    op.drop_table("test_entries_task_qcm")  # DATA LOSS
+    op.drop_table("test_entries_typing")  # DATA LOSS
+    op.drop_table("test_task_group_questions")
+    op.drop_table("test_task_groups")
+    op.drop_table("test_task_questions")
+    op.drop_table("test_task_questions_qcm")
+    op.drop_table("test_task_task_groups")
+    op.drop_table("test_tasks")
+    op.drop_table("tests")
+
+    ## Auto-generated tables:
+    #   survey_group_questions
+    #   survey_groups
+    #   survey_questions
+    #   survey_response_info
+    #   survey_responses
+    #   survey_survey_groups
+    #   survey_surveys
+    #   test_typing
+    #   test_typing_entry
diff --git a/backend/app/config.py b/backend/app/config.py
index b265224010bc73a768290fd4c17baee913a5e743..5d3620a1a3b2c04a294a0f74d337b7a3e2d2d035 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -1,6 +1,5 @@
 import os
 import secrets
-import logging
 
 # Database
 DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///../languagelab.sqlite")
diff --git a/backend/app/crud.py b/backend/app/crud/__init__.py
similarity index 53%
rename from backend/app/crud.py
rename to backend/app/crud/__init__.py
index 3cfb20fde9fe30249a03ecd47cdafcfe02875406..1a280d17d74e2db5b9235f927249906d9d737760 100644
--- a/backend/app/crud.py
+++ b/backend/app/crud/__init__.py
@@ -7,6 +7,9 @@ import models
 import schemas
 from hashing import Hasher
 
+from crud.tests import *
+from crud.studies import *
+
 
 def get_user(db: Session, user_id: int):
     return db.query(models.User).filter(models.User.id == user_id).first()
@@ -29,7 +32,7 @@ def get_users(db: Session, skip: int = 0):
     return db.query(models.User).offset(skip).all()
 
 
-def create_user(db: Session, user: schemas.UserCreate):
+def create_user(db: Session, user: schemas.UserCreate) -> models.User:
     password = Hasher.get_password_hash(user.password)
     nickname = user.nickname if user.nickname else user.email.split("@")[0]
     db_user = models.User(
@@ -279,227 +282,3 @@ def create_test_typing_entry(
     db.commit()
     db.refresh(db_entry)
     return db_entry
-
-
-def create_survey(db: Session, survey: schemas.SurveyCreate):
-    db_survey = models.SurveySurvey(**survey.dict())
-    db.add(db_survey)
-    db.commit()
-    db.refresh(db_survey)
-    return db_survey
-
-
-def get_survey(db: Session, survey_id: int):
-    return (
-        db.query(models.SurveySurvey)
-        .filter(models.SurveySurvey.id == survey_id)
-        .first()
-    )
-
-
-def get_surveys(db: Session, skip: int = 0):
-    return db.query(models.SurveySurvey).offset(skip).all()
-
-
-def delete_survey(db: Session, survey_id: int):
-    db.query(models.SurveySurvey).filter(models.SurveySurvey.id == survey_id).delete()
-    db.commit()
-
-
-def add_group_to_survey(db: Session, survey_id: int, group: schemas.SurveyGroup):
-    db_survey = (
-        db.query(models.SurveySurvey)
-        .filter(models.SurveySurvey.id == survey_id)
-        .first()
-    )
-    db_survey.groups.append(group)
-    db.commit()
-    db.refresh(db_survey)
-    return db_survey
-
-
-def remove_group_from_survey(db: Session, survey_id: int, group_id: int):
-    survey = (
-        db.query(models.SurveySurvey)
-        .filter(models.SurveySurvey.id == survey_id)
-        .first()
-    )
-
-    survey_group = (
-        db.query(models.SurveyGroup).filter(models.SurveyGroup.id == group_id).first()
-    )
-
-    if survey_group in survey.groups:
-        survey.groups.remove(survey_group)
-        db.commit()
-
-
-def create_survey_group(db: Session, survey_group: schemas.SurveyGroupCreate):
-    db_survey_group = models.SurveyGroup(**survey_group.dict())
-    db.add(db_survey_group)
-    db.commit()
-    db.refresh(db_survey_group)
-    return db_survey_group
-
-
-def get_survey_group(db: Session, survey_group_id: int) -> schemas.SurveyGroup:
-    return (
-        db.query(models.SurveyGroup)
-        .filter(models.SurveyGroup.id == survey_group_id)
-        .first()
-    )
-
-
-def get_survey_groups(db: Session, skip: int = 0):
-    return db.query(models.SurveyGroup).offset(skip).all()
-
-
-def delete_survey_group(db: Session, survey_group_id: int):
-    db.query(models.SurveyGroup).filter(
-        models.SurveyGroup.id == survey_group_id
-    ).delete()
-    db.commit()
-
-
-def add_item_to_survey_group(db: Session, group_id: int, item: models.SurveyQuestion):
-    db_survey_group = (
-        db.query(models.SurveyGroup).filter(models.SurveyGroup.id == group_id).first()
-    )
-    db_survey_group.questions.append(item)
-    db.commit()
-    db.refresh(db_survey_group)
-    return db_survey_group
-
-
-def remove_item_from_survey_group(db: Session, group_id: int, item_id: int):
-    survey_group = (
-        db.query(models.SurveyGroup).filter(models.SurveyGroup.id == group_id).first()
-    )
-
-    survey_question = (
-        db.query(models.SurveyQuestion)
-        .filter(models.SurveyQuestion.id == item_id)
-        .first()
-    )
-
-    if survey_question in survey_group.questions:
-        survey_group.questions.remove(survey_question)
-        db.commit()
-
-
-def create_survey_question(db: Session, survey_question: schemas.SurveyQuestionCreate):
-    db_survey_question = models.SurveyQuestion(**survey_question.dict())
-    db.add(db_survey_question)
-    db.commit()
-    db.refresh(db_survey_question)
-    return db_survey_question
-
-
-def get_survey_question(db: Session, survey_question_id: int):
-    return (
-        db.query(models.SurveyQuestion)
-        .filter(models.SurveyQuestion.id == survey_question_id)
-        .first()
-    )
-
-
-def get_survey_questions(db: Session, skip: int = 0):
-    return db.query(models.SurveyQuestion).offset(skip).all()
-
-
-def delete_survey_question(db: Session, survey_question_id: int):
-    db.query(models.SurveyQuestion).filter(
-        models.SurveyQuestion.id == survey_question_id
-    ).delete()
-    db.commit()
-
-
-def create_survey_response(db: Session, survey_response: schemas.SurveyResponseCreate):
-    db_survey_response = models.SurveyResponse(**survey_response.dict())
-    db.add(db_survey_response)
-    db.commit()
-    db.refresh(db_survey_response)
-    return db_survey_response
-
-
-def get_survey_responses(db: Session, sid: str, skip: int = 0):
-    return (
-        db.query(models.SurveyResponse)
-        .filter(models.SurveyResponse.sid == sid)
-        .offset(skip)
-        .all()
-    )
-
-
-def create_survey_response_info(
-    db: Session, survey_response_info: schemas.SurveyResponseInfoCreate
-):
-    db_survey_response_info = models.SurveyResponseInfo(**survey_response_info.dict())
-    db.add(db_survey_response_info)
-    db.commit()
-    db.refresh(db_survey_response_info)
-    return db_survey_response_info
-
-
-def create_study(db: Session, study: schemas.StudyCreate):
-    db_study = models.Study(**study.dict())
-    db.add(db_study)
-    db.commit()
-    db.refresh(db_study)
-    return db_study
-
-
-def get_study(db: Session, study_id: int):
-    return db.query(models.Study).filter(models.Study.id == study_id).first()
-
-
-def get_studies(db: Session, skip: int = 0):
-    return db.query(models.Study).offset(skip).all()
-
-
-def update_study(db: Session, study: schemas.StudyCreate, study_id: int):
-    db.query(models.Study).filter(models.Study.id == study_id).update(
-        {**study.model_dump(exclude_unset=True)}
-    )
-    db.commit()
-
-
-def delete_study(db: Session, study_id: int):
-    db.query(models.Study).filter(models.Study.id == study_id).delete()
-    db.commit()
-
-
-def add_user_to_study(db: Session, study_id: int, user: schemas.User):
-    db_study = db.query(models.Study).filter(models.Study.id == study_id).first()
-    db_study.users.append(user)
-    db.commit()
-    db.refresh(db_study)
-    return db_study
-
-
-def remove_user_from_study(db: Session, study_id: int, user: schemas.User):
-    study = db.query(models.Study).filter(models.Study.id == study_id).first()
-    user = db.query(models.User).filter(models.User.id == user.id).first()
-    study.users.remove(user)
-    db.commit()
-    return study
-
-
-def add_survey_to_study(db: Session, study_id: int, survey: schemas.Survey):
-    db_study = db.query(models.Study).filter(models.Study.id == study_id).first()
-    db_study.surveys.append(survey)
-    db.commit()
-    db.refresh(db_study)
-    return db_study
-
-
-def remove_survey_from_study(db: Session, study_id: int, survey: schemas.Survey):
-    study = db.query(models.Study).filter(models.Study.id == study_id).first()
-    survey = (
-        db.query(models.SurveySurvey)
-        .filter(models.SurveySurvey.id == survey.id)
-        .first()
-    )
-    study.surveys.remove(survey)
-    db.commit()
-    return study
diff --git a/backend/app/crud/studies.py b/backend/app/crud/studies.py
new file mode 100644
index 0000000000000000000000000000000000000000..561fa1de683eb84386ac835ff7defc7ab117f004
--- /dev/null
+++ b/backend/app/crud/studies.py
@@ -0,0 +1,166 @@
+from typing import Optional
+from sqlalchemy.orm import Session
+
+import models
+import schemas
+import crud
+from io import StringIO
+import csv
+from fastapi.responses import StreamingResponse
+from utils import extract_text_between_angle_bracket
+
+
+def create_study(db: Session, study: schemas.StudyCreate) -> models.Study:
+    db_study = models.Study(
+        **study.model_dump(exclude_unset=True, exclude={"user_ids", "test_ids"})
+    )
+
+    if study.model_fields_set & {"user_ids", "test_ids"}:
+        if db_study:
+            if "user_ids" in study.model_fields_set:
+                db_study.users = (
+                    db.query(models.User)
+                    .filter(models.User.id.in_(study.user_ids))
+                    .all()
+                )
+            if "test_ids" in study.model_fields_set:
+                db_study.tests = (
+                    db.query(models.Test)
+                    .filter(models.Test.id.in_(study.test_ids))
+                    .all()
+                )
+
+    db.add(db_study)
+    db.commit()
+    db.refresh(db_study)
+
+    return db_study
+
+
+def get_study(db: Session, study_id: int) -> Optional[models.Study]:
+    return db.query(models.Study).filter(models.Study.id == study_id).first()
+
+
+def get_studies(db: Session, skip: int = 0) -> list[models.Study]:
+    return db.query(models.Study).offset(skip).all()
+
+
+def update_study(db: Session, study: schemas.StudyCreate, study_id: int) -> None:
+    db.query(models.Study).filter(models.Study.id == study_id).update(
+        {**study.model_dump(exclude_unset=True, exclude={"user_ids", "test_ids"})}
+    )
+
+    if study.model_fields_set & {"user_ids", "test_ids"}:
+        if (
+            study_obj := db.query(models.Study)
+            .filter(models.Study.id == study_id)
+            .first()
+        ):
+            if "user_ids" in study.model_fields_set:
+                study_obj.users = (
+                    db.query(models.User)
+                    .filter(models.User.id.in_(study.user_ids))
+                    .all()
+                )
+            if "test_ids" in study.model_fields_set:
+                study_obj.tests = (
+                    db.query(models.Test)
+                    .filter(models.Test.id.in_(study.test_ids))
+                    .all()
+                )
+
+    db.commit()
+
+
+def delete_study(db: Session, study_id: int) -> None:
+    db.query(models.Study).filter(models.Study.id == study_id).delete()
+    db.commit()
+
+
+def download_study(db: Session, study_id: int):
+
+    output = StringIO()
+    writer = csv.writer(output)
+
+    header = [
+        "study_id",
+        "test_id",
+        "group_id",
+        "group_name",
+        "item_id",
+        "user_id",
+        "code",
+        "item_type",
+        "response",
+        "correct",
+        "response_time",
+    ]
+    writer.writerow(header)
+
+    db_entries = (
+        db.query(models.TestEntry).filter(models.TestEntry.study_id == study_id).all()
+    )
+
+    for entry in db_entries:
+        if entry.entry_task is None:
+            continue
+
+        test_id = entry.test_id
+        group_id = entry.entry_task.test_group_id
+        group = crud.get_group(db, group_id).title
+        item = entry.entry_task.test_question_id
+        user_id = entry.user_id
+        code = entry.code
+        response_time = entry.entry_task.response_time
+
+        if entry.entry_task.entry_task_qcm:
+            selected_id = entry.entry_task.entry_task_qcm.selected_id
+            correct_id = entry.entry_task.test_question.question_qcm.correct
+            correct_answer = int(selected_id == correct_id)
+
+            item_type = "qcm"
+            row = [
+                study_id,
+                test_id,
+                group_id,
+                group,
+                item,
+                user_id,
+                code,
+                item_type,
+                selected_id,
+                correct_answer,
+                response_time,
+            ]
+            writer.writerow(row)
+
+        if entry.entry_task.entry_task_gapfill:
+            answer = entry.entry_task.entry_task_gapfill.text
+            correct = extract_text_between_angle_bracket(
+                entry.entry_task.test_question.question
+            )
+            correct_answer = int(answer == correct)
+
+            item_type = "gapfill"
+            row = [
+                study_id,
+                test_id,
+                group_id,
+                group,
+                item,
+                user_id,
+                code,
+                item_type,
+                answer,
+                correct_answer,
+                response_time,
+            ]
+            writer.writerow(row)
+
+    output.seek(0)
+
+    return StreamingResponse(
+        output,
+        media_type="text/csv",
+        headers={"Content-Disposition": f"attachment; filename={study_id}-surveys.csv"},
+    )
diff --git a/backend/app/crud/tests.py b/backend/app/crud/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3297ff054e327a2e574ae72727dff7309defa40
--- /dev/null
+++ b/backend/app/crud/tests.py
@@ -0,0 +1,152 @@
+from sqlalchemy.orm import Session
+
+from utils import extract_text_between_angle_bracket
+import models
+import schemas
+
+
+def create_test(db: Session, test: schemas.TestCreate) -> models.Test:
+    db_test = models.Test(**test.model_dump())
+    db.add(db_test)
+    db.commit()
+    db.refresh(db_test)
+    return db_test
+
+
+def get_tests(db: Session, skip: int = 0) -> list[models.Test]:
+    return db.query(models.Test).offset(skip).all()
+
+
+def get_test(db: Session, test_id: int) -> models.Test | None:
+    return db.query(models.Test).filter(models.Test.id == test_id).first()
+
+
+def delete_test(db: Session, test_id: int) -> None:
+    db.query(models.Test).filter(models.Test.id == test_id).delete()
+    db.query(models.TestTask).filter(models.TestTask.test_id == test_id).delete()
+    db.query(models.TestTyping).filter(models.TestTyping.test_id == test_id).delete()
+    db.commit()
+
+
+def add_group_to_test_task(db: Session, test: models.Test, group: models.TestTaskGroup):
+    test.groups.append(group)
+    db.commit()
+    db.refresh(test)
+
+
+def remove_group_from_test_task(
+    db: Session, testTask: models.TestTask, group: models.TestTaskGroup
+):
+    try:
+        testTask.groups.remove(group)
+        db.commit()
+        db.refresh(testTask)
+    except ValueError:
+        pass
+
+
+def create_group(
+    db: Session, group: schemas.TestTaskGroupCreate
+) -> models.TestTaskGroup:
+    db_group = models.TestTaskGroup(**group.model_dump())
+    db.add(db_group)
+    db.commit()
+    db.refresh(db_group)
+    return db_group
+
+
+def get_group(db: Session, group_id: int) -> models.TestTaskGroup | None:
+    return (
+        db.query(models.TestTaskGroup)
+        .filter(models.TestTaskGroup.id == group_id)
+        .first()
+    )
+
+
+def delete_group(db: Session, group_id: int) -> None:
+    db.query(models.TestTaskGroup).filter(models.TestTaskGroup.id == group_id).delete()
+    db.commit()
+
+
+def add_question_to_group(
+    db: Session, group: models.TestTaskGroup, question: models.TestTaskQuestion
+):
+    group.questions.append(question)
+    db.commit()
+    db.refresh(group)
+
+
+def remove_question_from_group(
+    db: Session, group: models.TestTaskGroup, question: models.TestTaskQuestion
+):
+    try:
+        group.questions.remove(question)
+        db.commit()
+        db.refresh(group)
+    except ValueError:
+        pass
+
+
+def create_question(db: Session, question: schemas.TestTaskQuestionCreate):
+    db_question = models.TestTaskQuestion(**question.model_dump())
+    db.add(db_question)
+    db.commit()
+    db.refresh(db_question)
+    return db_question
+
+
+def get_question(db: Session, question_id: int):
+    return (
+        db.query(models.TestTaskQuestion)
+        .filter(models.TestTaskQuestion.id == question_id)
+        .first()
+    )
+
+
+def delete_question(db: Session, question_id: int):
+    db.query(models.TestTaskQuestion).filter(
+        models.TestTaskQuestion.id == question_id
+    ).delete()
+    db.query(models.TestTaskQuestionQCM).filter(
+        models.TestTaskQuestionQCM.question_id == question_id
+    ).delete()
+    db.commit()
+    return None
+
+
+def create_test_entry(db: Session, entry: schemas.TestEntryCreate):
+    db_entry = models.TestEntry(**entry.model_dump())
+    db.add(db_entry)
+    db.commit()
+    db.refresh(db_entry)
+    return db_entry
+
+
+def get_score(db: Session, rid: str):
+    db_entries = db.query(models.TestEntry).filter(models.TestEntry.rid == rid).all()
+
+    corrects = 0
+    total = 0
+
+    for entry in db_entries:
+        if entry.entry_task is None:
+            continue
+
+        total += 1
+
+        if entry.entry_task.entry_task_qcm:
+            selected_id = entry.entry_task.entry_task_qcm.selected_id
+            correct_id = entry.entry_task.test_question.question_qcm.correct
+            corrects += selected_id == correct_id
+
+        if entry.entry_task.entry_task_gapfill:
+            answer = entry.entry_task.entry_task_gapfill.text
+            correct = extract_text_between_angle_bracket(
+                entry.entry_task.test_question.question
+            )
+            corrects += answer == correct
+
+    if not total:
+        return 0
+
+    return corrects / total
diff --git a/backend/app/main.py b/backend/app/main.py
index d1a4cc36d5b041fcac04a0dba490560bf9448ead..ac9c7b2918fa444aaa85a7fca0091552beb2b841 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,6 +1,4 @@
 from collections import defaultdict
-import datetime
-from typing import Annotated
 from fastapi import (
     APIRouter,
     FastAPI,
@@ -8,7 +6,6 @@ from fastapi import (
     Depends,
     HTTPException,
     BackgroundTasks,
-    Header,
     Response,
 )
 from sqlalchemy.orm import Session
@@ -29,6 +26,8 @@ from database import Base, engine, get_db, SessionLocal
 from utils import check_user_level
 import config
 from security import jwt_cookie, get_jwt_user
+from routes.tests import testRouter
+from routes.studies import studiesRouter
 
 websocket_users = defaultdict(lambda: defaultdict(set))
 websocket_users_global = defaultdict(set)
@@ -72,8 +71,6 @@ usersRouter = APIRouter(prefix="/users", tags=["users"])
 sessionsRouter = APIRouter(prefix="/sessions", tags=["sessions"])
 studyRouter = APIRouter(prefix="/studies", tags=["studies"])
 websocketRouter = APIRouter(prefix="/ws", tags=["websocket"])
-webhookRouter = APIRouter(prefix="/webhooks", tags=["webhook"])
-surveyRouter = APIRouter(prefix="/surveys", tags=["surveys"])
 
 
 @v1Router.get("/health", status_code=status.HTTP_204_NO_CONTENT)
@@ -963,190 +960,6 @@ def propagate_presence(
     return
 
 
-@studyRouter.post("", status_code=status.HTTP_201_CREATED)
-def study_create(
-    study: schemas.StudyCreate,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401, detail="You do not have permission to create a study"
-        )
-    return crud.create_study(db, study).id
-
-
-@studyRouter.get("", response_model=list[schemas.Study])
-def studies_read(
-    db: Session = Depends(get_db),
-):
-    return crud.get_studies(db)
-
-
-@studyRouter.get("/{study_id}", response_model=schemas.Study)
-def study_read(
-    study_id: int,
-    db: Session = Depends(get_db),
-):
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-    return study
-
-
-@studyRouter.patch("/{study_id}", status_code=status.HTTP_204_NO_CONTENT)
-def study_update(
-    study_id: int,
-    studyUpdate: schemas.StudyCreate,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401, detail="You do not have permission to update a study"
-        )
-
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-
-    crud.update_study(db, studyUpdate, study_id)
-
-
-@studyRouter.delete("/{study_id}", status_code=status.HTTP_204_NO_CONTENT)
-def study_delete(
-    study_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401, detail="You do not have permission to delete a study"
-        )
-
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-
-    crud.delete_study(db, study_id)
-
-
-@studyRouter.post("/{study_id}/users/{user_id}", status_code=status.HTTP_201_CREATED)
-def study_add_user(
-    study_id: int,
-    user_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    user = crud.get_user(db, user_id)
-
-    if user != current_user and not check_user_level(
-        current_user, models.UserType.ADMIN
-    ):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add a user to a study",
-        )
-
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-
-    if user is None:
-        raise HTTPException(status_code=404, detail="User not found")
-
-    if user in study.users:
-        raise HTTPException(status_code=400, detail="User already exists in this study")
-
-    crud.add_user_to_study(db, study_id, user)
-
-
-@studyRouter.delete(
-    "/{study_id}/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT
-)
-def study_delete_user(
-    study_id: int,
-    user_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add a user to a study",
-        )
-
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-
-    user = crud.get_user(db, user_id)
-    if user is None:
-        raise HTTPException(status_code=404, detail="User not found")
-
-    if user not in study.users:
-        raise HTTPException(status_code=400, detail="User does not exist in this study")
-
-    crud.remove_user_from_study(db, study_id, user)
-
-
-@studyRouter.post(
-    "/{study_id}/surveys/{survey_id}", status_code=status.HTTP_201_CREATED
-)
-def study_add_survey(
-    study_id: int,
-    survey_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add a survey to a study",
-        )
-
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-    survey = crud.get_survey(db, survey_id)
-    if survey is None:
-        raise HTTPException(status_code=404, detail="Survey not found")
-    if survey in study.surveys:
-        raise HTTPException(
-            status_code=400, detail="Survey already exists in this study"
-        )
-
-    crud.add_survey_to_study(db, study_id, survey)
-
-
-@studyRouter.delete(
-    "/{study_id}/surveys/{survey_id}", status_code=status.HTTP_204_NO_CONTENT
-)
-def study_delete_survey(
-    study_id: int,
-    survey_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add a survey to a study",
-        )
-    study = crud.get_study(db, study_id)
-    if study is None:
-        raise HTTPException(status_code=404, detail="Study not found")
-    survey = crud.get_survey(db, survey_id)
-    if survey is None:
-        raise HTTPException(status_code=404, detail="Survey not found")
-    if survey not in study.surveys:
-        raise HTTPException(
-            status_code=400, detail="Survey does not exist in this study"
-        )
-
-    crud.remove_survey_from_study(db, study_id, survey)
-
-
 @websocketRouter.websocket("/sessions/{session_id}")
 async def websocket_session(
     session_id: int, token: str, websocket: WebSocket, db: Session = Depends(get_db)
@@ -1199,7 +1012,7 @@ async def websocket_session(
 
 
 @websocketRouter.websocket("/global")
-async def websocket_session(
+async def websocket_global(
     token: str, websocket: WebSocket, db: Session = Depends(get_db)
 ):
     try:
@@ -1237,356 +1050,12 @@ async def websocket_session(
             pass
 
 
-@webhookRouter.post("/sessions", status_code=status.HTTP_202_ACCEPTED)
-async def webhook_session(
-    webhook: schemas.CalComWebhook,
-    x_cal_signature_256: Annotated[str | None, Header()] = None,
-    db: Session = Depends(get_db),
-):
-
-    # TODO: Fix. Signature is a hash, not the secret
-    # https://cal.com/docs/core-features/webhooks#adding-a-custom-payload-template
-    # if x_cal_signature_256 != config.CALCOM_SECRET:
-    #    raise HTTPException(status_code=401, detail="Invalid secret")
-
-    if webhook.triggerEvent == "BOOKING_CREATED":
-        start_time = datetime.datetime.fromisoformat(webhook.payload["startTime"])
-        start_time -= datetime.timedelta(hours=1)
-        end_time = datetime.datetime.fromisoformat(webhook.payload["endTime"])
-        end_time += datetime.timedelta(hours=1)
-        attendes = webhook.payload["attendees"]
-        emails = [attendee["email"] for attendee in attendes if attendee != None]
-        print(emails)
-        db_users = [
-            crud.get_user_by_email(db, email) for email in emails if email != None
-        ]
-        print(db_users)
-        users = [user for user in db_users if user != None]
-
-        if users:
-            db_session = crud.create_session_with_users(db, users, start_time, end_time)
-        else:
-            raise HTTPException(status_code=404, detail="Users not found")
-
-        return
-
-    raise HTTPException(status_code=400, detail="Invalid trigger event")
-
-
-@surveyRouter.post("", status_code=status.HTTP_201_CREATED)
-def create_survey(
-    survey: schemas.SurveyCreate,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401, detail="You do not have permission to create a survey"
-        )
-
-    return crud.create_survey(db, survey).id
-
-
-@surveyRouter.get("", response_model=list[schemas.Survey])
-def get_surveys(
-    db: Session = Depends(get_db),
-):
-    return crud.get_surveys(db)
-
-
-@surveyRouter.get("/{survey_id}", response_model=schemas.Survey)
-def get_survey(
-    survey_id: int,
-    db: Session = Depends(get_db),
-):
-    survey = crud.get_survey(db, survey_id)
-    if survey is None:
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    return survey
-
-
-@surveyRouter.delete("/{survey_id}", status_code=status.HTTP_204_NO_CONTENT)
-def delete_survey(
-    survey_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401, detail="You do not have permission to delete a survey"
-        )
-
-    crud.delete_survey(db, survey_id)
-
-
-@surveyRouter.post("/{survey_id}/groups", status_code=status.HTTP_201_CREATED)
-def add_group_to_survey(
-    survey_id: int,
-    groupc: schemas.SurveySurveyAddGroup,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add a group to a survey",
-        )
-
-    survey = crud.get_survey(db, survey_id)
-    if survey is None:
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    group = crud.get_survey_group(db, groupc.group_id)
-    if group is None:
-        raise HTTPException(status_code=404, detail="Survey group not found")
-
-    return crud.add_group_to_survey(db, survey_id, group)
-
-
-@surveyRouter.delete(
-    "/{survey_id}/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT
-)
-def remove_group_from_survey(
-    survey_id: int,
-    group_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to remove a group from a survey",
-        )
-
-    if not crud.get_survey(db, survey_id):
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    if not crud.get_survey_group(db, group_id):
-        raise HTTPException(status_code=404, detail="Survey group not found")
-
-    crud.remove_group_from_survey(db, survey_id, group_id)
-
-
-@surveyRouter.post("/groups", status_code=status.HTTP_201_CREATED)
-def create_survey_group(
-    group: schemas.SurveyGroupCreate,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to create a survey group",
-        )
-
-    return crud.create_survey_group(db, group).id
-
-
-@surveyRouter.get("/groups", response_model=list[schemas.SurveyGroup])
-def get_survey_groups(
-    db: Session = Depends(get_db),
-):
-    return crud.get_survey_groups(db)
-
-
-@surveyRouter.get("/groups/{group_id}", response_model=schemas.SurveyGroup)
-def get_survey_group(
-    group_id: int,
-    db: Session = Depends(get_db),
-):
-    group = crud.get_survey_group(db, group_id)
-    if group is None:
-        raise HTTPException(status_code=404, detail="Survey group not found")
-
-    return group
-
-
-@surveyRouter.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
-def delete_survey_group(
-    group_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to delete a survey group",
-        )
-
-    if not crud.get_survey_group(db, group_id):
-        raise HTTPException(status_code=404, detail="Survey group not found")
-
-    crud.delete_survey_group(db, group_id)
-
-
-@surveyRouter.post("/groups/{group_id}/items", status_code=status.HTTP_201_CREATED)
-def add_item_to_survey_group(
-    group_id: int,
-    question: schemas.SurveyGroupAddQuestion,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to add an item to a survey group",
-        )
-
-    item = crud.get_survey_question(db, question.question_id)
-    if item is None:
-        raise HTTPException(status_code=404, detail="Survey question not found")
-
-    return crud.add_item_to_survey_group(db, group_id, item)
-
-
-@surveyRouter.delete(
-    "/groups/{group_id}/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT
-)
-def remove_item_from_survey_group(
-    group_id: int,
-    item_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to remove an item from a survey group",
-        )
-
-    if not crud.get_survey_group(db, group_id):
-        raise HTTPException(status_code=404, detail="Survey group not found")
-
-    if not crud.get_survey_question(db, item_id):
-        raise HTTPException(status_code=404, detail="Survey question not found")
-
-    crud.remove_item_from_survey_group(db, group_id, item_id)
-
-
-@surveyRouter.post("/items", status_code=status.HTTP_201_CREATED)
-def create_survey_question(
-    question: schemas.SurveyQuestionCreate,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to create a survey question",
-        )
-
-    return crud.create_survey_question(db, question).id
-
-
-@surveyRouter.get("/items", response_model=list[schemas.SurveyQuestion])
-def get_survey_questions(
-    db: Session = Depends(get_db),
-):
-    return crud.get_survey_questions(db)
-
-
-@surveyRouter.get("/items/{question_id}", response_model=schemas.SurveyQuestion)
-def get_survey_question(
-    question_id: int,
-    db: Session = Depends(get_db),
-):
-    question = crud.get_survey_question(db, question_id)
-    if question is None:
-        raise HTTPException(status_code=404, detail="Survey question not found")
-
-    return question
-
-
-@surveyRouter.delete("/items/{question_id}", status_code=status.HTTP_204_NO_CONTENT)
-def delete_survey_question(
-    question_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to delete a survey question",
-        )
-
-    if not crud.get_survey_question(db, question_id):
-        raise HTTPException(status_code=404, detail="Survey question not found")
-
-    crud.delete_survey_question(db, question_id)
-
-
-@surveyRouter.post("/responses", status_code=status.HTTP_201_CREATED)
-def create_survey_response(
-    response: schemas.SurveyResponseCreate,
-    db: Session = Depends(get_db),
-):
-    return crud.create_survey_response(db, response).id
-
-
-@surveyRouter.get("/responses/{survey_id}", response_model=list[schemas.SurveyResponse])
-def get_survey_responses(
-    survey_id: int,
-    db: Session = Depends(get_db),
-    current_user: schemas.User = Depends(get_jwt_user),
-):
-    if not check_user_level(current_user, models.UserType.ADMIN):
-        raise HTTPException(
-            status_code=401,
-            detail="You do not have permission to view survey responses",
-        )
-
-    if not crud.get_survey(db, survey_id):
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    return crud.get_survey_responses(db, survey_id)
-
-
-@surveyRouter.post("/info/{survey_id}", status_code=status.HTTP_201_CREATED)
-def create_survey_info(
-    survey_id: int,
-    info: schemas.SurveyResponseInfoCreate,
-    db: Session = Depends(get_db),
-):
-    if not crud.get_survey(db, survey_id):
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    return crud.create_survey_response_info(db, info)
-
-
-@surveyRouter.get("/{survey_id}/score/{sid}", response_model=dict)
-def get_survey_score(
-    survey_id: int,
-    sid: str,
-    db: Session = Depends(get_db),
-):
-    if not crud.get_survey(db, survey_id):
-        raise HTTPException(status_code=404, detail="Survey not found")
-
-    responses = crud.get_survey_responses(db, sid)
-
-    score = 0
-    total = 0
-    for response in responses:
-        question = crud.get_survey_question(db, response.question_id)
-        if not question:
-            continue
-        total += 1
-        if response.selected_id == question.correct:
-            score += 1
-
-    return {
-        "survey_id": survey_id,
-        "score": round((score / total) * 100 if total > 0 else 0, 2),
-    }
-
-
 v1Router.include_router(authRouter)
 v1Router.include_router(usersRouter)
 v1Router.include_router(sessionsRouter)
 v1Router.include_router(studyRouter)
 v1Router.include_router(websocketRouter)
-v1Router.include_router(webhookRouter)
-v1Router.include_router(surveyRouter)
+v1Router.include_router(testRouter)
+v1Router.include_router(studiesRouter)
 apiRouter.include_router(v1Router)
 app.include_router(apiRouter)
diff --git a/backend/app/models.py b/backend/app/models/__init__.py
similarity index 56%
rename from backend/app/models.py
rename to backend/app/models/__init__.py
index 2ae862f2fd9f8418dbd86a5d2d0026e18c56e252..131af45538f6e3c6e8ea62551bd1fee65989250a 100644
--- a/backend/app/models.py
+++ b/backend/app/models/__init__.py
@@ -1,4 +1,5 @@
 from sqlalchemy import (
+    JSON,
     Column,
     Float,
     Integer,
@@ -14,7 +15,9 @@ from enum import Enum
 from database import Base
 import datetime
 from utils import datetime_aware
-from sqlalchemy.dialects.postgresql import JSON
+
+from models.studies import *
+from models.tests import *
 
 
 class UserType(Enum):
@@ -180,134 +183,3 @@ class MessageFeedback(Base):
 
     def raw(self):
         return [self.id, self.message_id, self.start, self.end, self.content, self.date]
-
-
-class TestTyping(Base):
-    __tablename__ = "test_typing"
-
-    id = Column(Integer, primary_key=True, index=True)
-    created_at = Column(DateTime, default=datetime_aware)
-    code = Column(String)
-    entries = relationship("TestTypingEntry", backref="typing")
-
-
-class TestTypingEntry(Base):
-    __tablename__ = "test_typing_entry"
-
-    id = Column(Integer, primary_key=True, index=True)
-    typing_id = Column(Integer, ForeignKey("test_typing.id"), index=True)
-    exerciceId = Column(Integer)
-    position = Column(Integer)
-    downtime = Column(Integer)
-    uptime = Column(Integer)
-    keyCode = Column(Integer)
-    keyValue = Column(String)
-
-
-class SurveyGroupQuestion(Base):
-    __tablename__ = "survey_group_questions"
-
-    group_id = Column(Integer, ForeignKey("survey_groups.id"), primary_key=True)
-    question_id = Column(Integer, ForeignKey("survey_questions.id"), primary_key=True)
-
-
-class SurveyQuestion(Base):
-    __tablename__ = "survey_questions"
-
-    id = Column(Integer, primary_key=True, index=True)
-    question = Column(String)
-    correct = Column(Integer)
-    option1 = Column(String)
-    option2 = Column(String)
-    option3 = Column(String)
-    option4 = Column(String)
-    option5 = Column(String)
-    option6 = Column(String)
-    option7 = Column(String)
-    option8 = Column(String)
-
-
-class SurveyGroup(Base):
-    __tablename__ = "survey_groups"
-
-    id = Column(Integer, primary_key=True, index=True)
-    title = Column(String)
-    demo = Column(String, default=False)
-    questions = relationship(
-        "SurveyQuestion", secondary="survey_group_questions", backref="group"
-    )
-
-
-class SurveySurvey(Base):
-    __tablename__ = "survey_surveys"
-
-    id = Column(Integer, primary_key=True, index=True)
-    title = Column(String)
-    groups = relationship(
-        "SurveyGroup", secondary="survey_survey_groups", backref="survey"
-    )
-    studies = relationship("Study", secondary="study_surveys", back_populates="surveys")
-
-
-class SurveySurveyGroup(Base):
-    __tablename__ = "survey_survey_groups"
-
-    survey_id = Column(Integer, ForeignKey("survey_surveys.id"), primary_key=True)
-    group_id = Column(Integer, ForeignKey("survey_groups.id"), primary_key=True)
-
-
-class SurveyResponse(Base):
-    __tablename__ = "survey_responses"
-
-    id = Column(Integer, primary_key=True, index=True)
-    code = Column(String)
-    sid = Column(String)
-    uid = Column(Integer, ForeignKey("users.id"), default=None)
-    created_at = Column(DateTime, default=datetime_aware)
-    survey_id = Column(Integer, ForeignKey("survey_surveys.id"))
-    group_id = Column(Integer, ForeignKey("survey_groups.id"))
-    question_id = Column(Integer, ForeignKey("survey_questions.id"))
-    selected_id = Column(Integer)
-    response_time = Column(Float)
-    text = Column(String)
-
-
-class SurveyResponseInfo(Base):
-    __tablename__ = "survey_response_info"
-
-    id = Column(Integer, primary_key=True, index=True)
-    sid = Column(String)
-    birthyear = Column(Integer)
-    gender = Column(String)
-    primary_language = Column(String)
-    other_language = Column(String)
-    education = Column(String)
-
-
-class Study(Base):
-    __tablename__ = "studies"
-    id = Column(Integer, primary_key=True, index=True)
-    title = Column(String)
-    description = Column(String)
-    start_date = Column(DateTime)
-    end_date = Column(DateTime)
-    chat_duration = Column(Integer)
-
-    users = relationship("User", secondary="study_users", back_populates="studies")
-    surveys = relationship(
-        "SurveySurvey", secondary="study_surveys", back_populates="studies"
-    )
-
-
-class StudyUser(Base):
-    __tablename__ = "study_users"
-
-    study_id = Column(Integer, ForeignKey("studies.id"), primary_key=True)
-    user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
-
-
-class StudySurvey(Base):
-    __tablename__ = "study_surveys"
-
-    study_id = Column(Integer, ForeignKey("studies.id"), primary_key=True)
-    survey_id = Column(Integer, ForeignKey("survey_surveys.id"), primary_key=True)
diff --git a/backend/app/models/studies.py b/backend/app/models/studies.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd0db73b343c7a28b8339d8b5348f0afbf7fdf40
--- /dev/null
+++ b/backend/app/models/studies.py
@@ -0,0 +1,41 @@
+from database import Base
+
+from sqlalchemy import (
+    Column,
+    Integer,
+    String,
+    ForeignKey,
+    DateTime,
+)
+from sqlalchemy.orm import relationship
+
+
+class Study(Base):
+    __tablename__ = "studies"
+    id = Column(Integer, primary_key=True, index=True)
+    title = Column(String)
+    description = Column(String)
+    start_date = Column(DateTime)
+    end_date = Column(DateTime)
+    nb_session = Column(Integer)
+    consent_participation = Column(String)
+    consent_privacy = Column(String)
+    consent_rights = Column(String)
+    consent_study_data = Column(String)
+
+    users = relationship("User", secondary="study_users")
+    tests = relationship("Test", secondary="study_tests")
+
+
+class StudyUser(Base):
+    __tablename__ = "study_users"
+
+    study_id = Column(Integer, ForeignKey("studies.id"), primary_key=True)
+    user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
+
+
+class StudyTest(Base):
+    __tablename__ = "study_tests"
+
+    study_id = Column(Integer, ForeignKey("studies.id"), primary_key=True)
+    test_id = Column(Integer, ForeignKey("tests.id"), primary_key=True)
diff --git a/backend/app/models/tests.py b/backend/app/models/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..35409101ea16aefd77290f9abdcff9c760190d6c
--- /dev/null
+++ b/backend/app/models/tests.py
@@ -0,0 +1,276 @@
+from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String
+from sqlalchemy.orm import relationship, validates
+from utils import datetime_aware
+from database import Base
+
+
+class TestTyping(Base):
+    __tablename__ = "test_typings"
+
+    test_id = Column(Integer, ForeignKey("tests.id"), primary_key=True)
+    explanations = Column(String, nullable=True)
+    text = Column(String, nullable=False)
+    repeat = Column(Integer, nullable=False, default=1)
+    duration = Column(Integer, nullable=False, default=0)
+
+    test = relationship(
+        "Test", uselist=False, back_populates="test_typing", lazy="selectin"
+    )
+
+
+class TestTask(Base):
+    __tablename__ = "test_tasks"
+    test_id = Column(Integer, ForeignKey("tests.id"), primary_key=True)
+
+    test = relationship(
+        "Test", uselist=False, back_populates="test_task", lazy="selectin"
+    )
+    groups = relationship(
+        "TestTaskGroup",
+        secondary="test_task_task_groups",
+        lazy="selectin",
+    )
+
+
+class Test(Base):
+    __tablename__ = "tests"
+
+    id = Column(Integer, primary_key=True, index=True)
+    title = Column(String, nullable=False)
+
+    test_typing = relationship(
+        "TestTyping",
+        uselist=False,
+        back_populates="test",
+        lazy="selectin",
+    )
+    test_task = relationship(
+        "TestTask",
+        uselist=False,
+        back_populates="test",
+        lazy="selectin",
+    )
+
+    @validates("test_typing")
+    def adjust_test_typing(self, _, value) -> TestTyping | None:
+        if value:
+            if isinstance(value, dict):
+                return TestTyping(**value, test_id=self.id)
+            else:
+                value.test_id = self.id
+                return value
+
+    @validates("test_task")
+    def adjust_test_task(self, _, value) -> TestTask | None:
+        if value:
+            if isinstance(value, dict):
+                return TestTask(**value, test_id=self.id)
+            else:
+                value.test_id = self.id
+                return value
+
+
+class TestTaskTaskGroup(Base):
+    __tablename__ = "test_task_task_groups"
+
+    test_task_id = Column(Integer, ForeignKey("test_tasks.test_id"), primary_key=True)
+    group_id = Column(Integer, ForeignKey("test_task_groups.id"), primary_key=True)
+
+
+class TestTaskGroup(Base):
+    __tablename__ = "test_task_groups"
+    id = Column(Integer, primary_key=True, index=True)
+
+    title = Column(String, nullable=False)
+    demo = Column(Boolean, default=False)
+    randomize = Column(Boolean, default=True)
+
+    questions = relationship(
+        "TestTaskQuestion",
+        secondary="test_task_group_questions",
+        lazy="selectin",
+    )
+
+
+class TestTaskGroupQuestion(Base):
+    __tablename__ = "test_task_group_questions"
+    group_id = Column(Integer, ForeignKey("test_task_groups.id"), primary_key=True)
+    question_id = Column(
+        Integer, ForeignKey("test_task_questions.id"), primary_key=True
+    )
+
+
+class TestTaskQuestionQCM(Base):
+    __tablename__ = "test_task_questions_qcm"
+
+    question_id = Column(
+        Integer, ForeignKey("test_task_questions.id"), primary_key=True
+    )
+
+    correct = Column(Integer, nullable=True)
+    option1 = Column(String, nullable=True)
+    option2 = Column(String, nullable=True)
+    option3 = Column(String, nullable=True)
+    option4 = Column(String, nullable=True)
+    option5 = Column(String, nullable=True)
+    option6 = Column(String, nullable=True)
+    option7 = Column(String, nullable=True)
+    option8 = Column(String, nullable=True)
+
+    question = relationship("TestTaskQuestion", back_populates="question_qcm")
+
+
+class TestTaskQuestion(Base):
+    __tablename__ = "test_task_questions"
+    id = Column(Integer, primary_key=True, index=True)
+    question = Column(String, nullable=True)
+
+    question_qcm = relationship(
+        "TestTaskQuestionQCM",
+        uselist=False,
+        back_populates="question",
+        lazy="selectin",
+    )
+
+    @validates("question_qcm")
+    def adjust_question_qcm(self, _, value) -> TestTaskQuestionQCM | None:
+        if value:
+            if isinstance(value, dict):
+                return TestTaskQuestionQCM(**value, question_id=self.id)
+            else:
+                value.question_id = self.id
+                return value
+
+
+class TestEntryTaskQCM(Base):
+    __tablename__ = "test_entries_task_qcm"
+
+    entry_id = Column(
+        Integer, ForeignKey("test_entries_task.entry_id"), primary_key=True
+    )
+    selected_id = Column(Integer, nullable=False)
+
+    entry_task = relationship(
+        "TestEntryTask",
+        uselist=False,
+        back_populates="entry_task_qcm",
+        lazy="selectin",
+    )
+
+
+class TestEntryTaskGapfill(Base):
+    __tablename__ = "test_entries_task_gapfill"
+
+    entry_id = Column(
+        Integer, ForeignKey("test_entries_task.entry_id"), primary_key=True, index=True
+    )
+    text = Column(String, nullable=False)
+
+    entry_task = relationship(
+        "TestEntryTask",
+        uselist=False,
+        back_populates="entry_task_gapfill",
+        lazy="selectin",
+    )
+
+
+class TestEntryTask(Base):
+    __tablename__ = "test_entries_task"
+
+    entry_id = Column(
+        Integer, ForeignKey("test_entries.id"), primary_key=True, index=True
+    )
+
+    test_group_id = Column(Integer, ForeignKey("test_task_groups.id"), index=True)
+    test_question_id = Column(Integer, ForeignKey("test_task_questions.id"), index=True)
+    response_time = Column(Float, nullable=False)
+
+    entry_task_qcm = relationship(
+        "TestEntryTaskQCM",
+        uselist=False,
+        back_populates="entry_task",
+        lazy="selectin",
+    )
+    entry_task_gapfill = relationship(
+        "TestEntryTaskGapfill",
+        uselist=False,
+        back_populates="entry_task",
+        lazy="selectin",
+    )
+
+    test_question = relationship("TestTaskQuestion", uselist=False, lazy="selectin")
+
+    entry = relationship("TestEntry", uselist=False, back_populates="entry_task")
+
+    @validates("entry_task_qcm")
+    def adjust_entry_qcm(self, _, value) -> TestEntryTaskQCM | None:
+        if value:
+            if isinstance(value, dict):
+                return TestEntryTaskQCM(**value, entry_id=self.entry_id)
+            else:
+                value.entry_id = self.entry_id
+                return value
+
+    @validates("entry_task_gapfill")
+    def adjust_entry_gapfill(self, _, value) -> TestEntryTaskGapfill | None:
+        if value:
+            if isinstance(value, dict):
+                return TestEntryTaskGapfill(**value, entry_id=self.entry_id)
+            else:
+                value.entry_id = self.entry_id
+                return value
+
+
+class TestEntryTyping(Base):
+    __tablename__ = "test_entries_typing"
+
+    entry_id = Column(
+        Integer, ForeignKey("test_entries.id"), primary_key=True, index=True
+    )
+
+    position = Column(Integer, nullable=False)
+    downtime = Column(Integer, nullable=False)
+    uptime = Column(Integer, nullable=False)
+    key_code = Column(Integer, nullable=False)
+    key_value = Column(String, nullable=False)
+
+    entry = relationship(
+        "TestEntry", uselist=False, back_populates="entry_typing", lazy="selectin"
+    )
+
+
+class TestEntry(Base):
+    __tablename__ = "test_entries"
+
+    id = Column(Integer, primary_key=True, index=True)
+    code = Column(String, nullable=False)
+    rid = Column(String, nullable=True)
+    user_id = Column(Integer, ForeignKey("users.id"), default=None, nullable=True)
+    test_id = Column(Integer, ForeignKey("tests.id"), nullable=False)
+    study_id = Column(Integer, ForeignKey("studies.id"), nullable=False)
+    created_at = Column(DateTime, default=datetime_aware)
+
+    entry_task = relationship(
+        "TestEntryTask", uselist=False, back_populates="entry", lazy="selectin"
+    )
+    entry_typing = relationship(
+        "TestEntryTyping", uselist=False, back_populates="entry", lazy="selectin"
+    )
+
+    @validates("entry_task")
+    def adjust_entry_task(self, _, value) -> TestEntryTask | None:
+        if value:
+            if isinstance(value, dict):
+                return TestEntryTask(**value, entry_id=self.id)
+            else:
+                value.entry_id = self.id
+                return value
+
+    @validates("entry_typing")
+    def adjust_entry_typing(self, _, value) -> TestEntryTyping | None:
+        if value:
+            if isinstance(value, dict):
+                return TestEntryTyping(**value, entry_id=self.id)
+            else:
+                value.entry_id = self.id
+                return value
diff --git a/frontend/src/routes/tests/typing/+page.server.ts b/backend/app/routes/__init__.py
similarity index 100%
rename from frontend/src/routes/tests/typing/+page.server.ts
rename to backend/app/routes/__init__.py
diff --git a/backend/app/routes/decorators.py b/backend/app/routes/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..6cd315b4f26303f90261d449ff3eff78334b1408
--- /dev/null
+++ b/backend/app/routes/decorators.py
@@ -0,0 +1,21 @@
+from typing import Callable
+
+from fastapi import HTTPException
+
+import schemas
+from utils import check_user_level
+
+
+def require_admin(error: str):
+    def decorator(func: Callable):
+        def wrapper(*args, current_user: schemas.User, **kwargs):
+            if not check_user_level(current_user, schemas.UserType.ADMIN):
+                raise HTTPException(
+                    status_code=401,
+                    detail=error,
+                )
+            return func(*args, current_user=current_user, **kwargs)
+
+        return wrapper
+
+    return decorator
diff --git a/backend/app/routes/studies.py b/backend/app/routes/studies.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cba51e4d8b1a6219c68d9d9d02a18d74846349a
--- /dev/null
+++ b/backend/app/routes/studies.py
@@ -0,0 +1,73 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+
+import crud
+import schemas
+from database import get_db
+from models import Session
+from routes.decorators import require_admin
+
+
+studiesRouter = APIRouter(prefix="/studies", tags=["Studies"])
+
+
+@require_admin("You do not have permission to create a study.")
+@studiesRouter.post("", status_code=status.HTTP_201_CREATED)
+def create_study(
+    study: schemas.StudyCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.create_study(db, study).id
+
+
+@studiesRouter.get("", response_model=list[schemas.Study])
+def get_studies(
+    skip: int = 0,
+    db: Session = Depends(get_db),
+):
+    return crud.get_studies(db, skip)
+
+
+@studiesRouter.get("/{study_id}", response_model=schemas.Study)
+def get_study(
+    study_id: int,
+    db: Session = Depends(get_db),
+):
+    study = crud.get_study(db, study_id)
+
+    if study is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Study not found"
+        )
+
+    return study
+
+
+@require_admin("You do not have permission to patch a study.")
+@studiesRouter.patch("/{study_id}", status_code=status.HTTP_204_NO_CONTENT)
+def update_study(
+    study_id: int,
+    study: schemas.StudyCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.update_study(db, study, study_id)
+
+
+@require_admin("You do not have permission to delete a study.")
+@studiesRouter.delete("/{study_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_study(
+    study_id: int,
+    db: Session = Depends(get_db),
+):
+    return crud.delete_study(db, study_id)
+
+
+@require_admin("You do not have permission to download this study.")
+@studiesRouter.get("/{study_id}/download/surveys")
+def download_study(
+    study_id: int,
+    db: Session = Depends(get_db),
+):
+    study = crud.get_study(db, study_id)
+    if study is None:
+        raise HTTPException(status_code=404, detail="Study not found")
+    return crud.download_study(db, study_id)
diff --git a/backend/app/routes/tests.py b/backend/app/routes/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..f310d546cfad88c57636aad32c2d14aaa6ac4c26
--- /dev/null
+++ b/backend/app/routes/tests.py
@@ -0,0 +1,200 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from starlette.status import HTTP_200_OK
+
+import crud
+import schemas
+from database import get_db
+from routes.decorators import require_admin
+
+testRouter = APIRouter(prefix="/tests", tags=["Tests"])
+
+
+@require_admin("You do not have permission to create a test.")
+@testRouter.post("", status_code=status.HTTP_201_CREATED)
+def create_test(
+    test: schemas.TestCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.create_test(db, test).id
+
+
+@testRouter.get("")
+def get_tests(
+    skip: int = 0,
+    db: Session = Depends(get_db),
+):
+    return crud.get_tests(db, skip)
+
+
+@testRouter.get("/{test_id}", response_model=schemas.Test)
+def get_test(
+    test_id: int,
+    db: Session = Depends(get_db),
+):
+    test = crud.get_test(db, test_id)
+    if test is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Test not found"
+        )
+
+    return test
+
+
+@require_admin("You do not have permission to: delete a test.")
+@testRouter.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_test(
+    test_id: int,
+    db: Session = Depends(get_db),
+):
+    return crud.delete_test(db, test_id)
+
+
+@require_admin("You do not have permission to add a group to a task test.")
+@testRouter.post("/{test_id}/groups/{group_id}", status_code=status.HTTP_201_CREATED)
+def add_group_to_test(
+    test_id: int,
+    group_id: int,
+    db: Session = Depends(get_db),
+):
+    test = crud.get_test(db, test_id)
+    if test is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Test not found"
+        )
+    if test.test_task is None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Test does not have a task",
+        )
+
+    group = crud.get_group(db, group_id)
+    if group is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
+        )
+
+    return crud.add_group_to_test_task(db, test.test_task, group)
+
+
+@require_admin("You do not have permission to remove a group from a task test.")
+@testRouter.delete(
+    "/{test_id}/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT
+)
+def remove_group_from_test(
+    test_id: int,
+    group_id: int,
+    db: Session = Depends(get_db),
+):
+    test = crud.get_test(db, test_id)
+    if test is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Test not found"
+        )
+    if test.test_task is None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Test does not have a task",
+        )
+    group = crud.get_group(db, group_id)
+    if group is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
+        )
+    return crud.remove_group_from_test_task(db, test.test_task, group)
+
+
+@require_admin("You do not have permission to create a group.")
+@testRouter.post("/groups", status_code=status.HTTP_201_CREATED)
+def create_group(
+    group: schemas.TestTaskGroupCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.create_group(db, group).id
+
+
+@require_admin("You do not have permission to delete a group.")
+@testRouter.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_group(
+    group_id: int,
+    db: Session = Depends(get_db),
+):
+    return crud.delete_group(db, group_id)
+
+
+@require_admin("You do not have permission to add a question to a group.")
+@testRouter.post(
+    "/groups/{group_id}/questions/{question_id}", status_code=status.HTTP_201_CREATED
+)
+def add_question_to_group(
+    group_id: int,
+    question_id: int,
+    db: Session = Depends(get_db),
+):
+    group = crud.get_group(db, group_id)
+    if group is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
+        )
+    question = crud.get_question(db, question_id)
+    if question is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Question not found"
+        )
+    return crud.add_question_to_group(db, group, question)
+
+
+@require_admin("You do not have permission to remove a question from a group.")
+@testRouter.delete(
+    "/groups/{group_id}/questions/{question_id}", status_code=status.HTTP_204_NO_CONTENT
+)
+def remove_question_from_group(
+    group_id: int,
+    question_id: int,
+    db: Session = Depends(get_db),
+):
+    group = crud.get_group(db, group_id)
+    if group is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
+        )
+    question = crud.get_question(db, question_id)
+    if question is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Question not found"
+        )
+    return crud.remove_question_from_group(db, group, question)
+
+
+@require_admin("You do not have permission to create a question.")
+@testRouter.post("/questions", status_code=status.HTTP_201_CREATED)
+def create_question(
+    question: schemas.TestTaskQuestionCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.create_question(db, question).id
+
+
+@require_admin("You do not have permission to delete a question.")
+@testRouter.delete("/questions/{question_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_question(
+    question_id: int,
+    db: Session = Depends(get_db),
+):
+    return crud.delete_question(db, question_id)
+
+
+@testRouter.post("/entries", status_code=status.HTTP_201_CREATED)
+def create_entry(
+    entry: schemas.TestEntryCreate,
+    db: Session = Depends(get_db),
+):
+    return crud.create_test_entry(db, entry).id
+
+
+@testRouter.get("/entries/score/{rid}", status_code=status.HTTP_200_OK)
+def get_score(
+    rid: str,
+    db: Session = Depends(get_db),
+):
+    return crud.get_score(db, rid)
diff --git a/backend/app/schemas.py b/backend/app/schemas/__init__.py
similarity index 67%
rename from backend/app/schemas.py
rename to backend/app/schemas/__init__.py
index ea40fd0188f79b0031dfe206bb7edf6e6e078e1a..f8dbff79fa40cb009ea682bec71c6882d439ab38 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas/__init__.py
@@ -1,6 +1,8 @@
 from pydantic import BaseModel, NaiveDatetime
 
-from models import UserType
+from schemas.studies import *
+from schemas.tests import *
+from schemas.users import *
 
 
 class LoginData(BaseModel):
@@ -15,86 +17,6 @@ class RegisterData(BaseModel):
     is_tutor: bool
 
 
-class User(BaseModel):
-    id: int
-    email: str
-    nickname: str
-    type: int
-    bio: str | None
-    is_active: bool
-    ui_language: str | None
-    home_language: str | None
-    target_language: str | None
-    birthdate: NaiveDatetime | None
-    gender: str | None = None
-    calcom_link: str | None
-    last_survey: NaiveDatetime | None = None
-    availabilities: list[dict] | None = []
-    tutor_list: list[str] | None = []
-    my_tutor: str | None = None
-    my_slots: list[dict] | None = []
-
-    class Config:
-        from_attributes = True
-
-    def to_dict(self):
-        return {
-            "id": self.id,
-            "email": self.email,
-            "nickname": self.nickname,
-            "type": self.type,
-            "availability": self.availability,
-            "is_active": self.is_active,
-            "ui_language": self.ui_language,
-            "home_language": self.home_language,
-            "target_language": self.target_language,
-            "birthdate": self.birthdate.isoformat() if self.birthdate else None,
-        }
-
-
-class UserCreate(BaseModel):
-    email: str
-    nickname: str | None = None
-    password: str
-    type: int = UserType.STUDENT.value
-    bio: str | None = None
-    is_active: bool = True
-    ui_language: str | None = None
-    home_language: str | None = None
-    target_language: str | None = None
-    birthdate: NaiveDatetime | None = None
-    gender: str | None = None
-    calcom_link: str | None = None
-    last_survey: NaiveDatetime | None = None
-    availabilities: list[dict] | None = []
-    tutor_list: list[str] | None = []
-    my_tutor: str | None = None
-    my_slots: list[dict] | None = []
-
-
-class UserUpdate(BaseModel):
-    email: str | None = None
-    nickname: str | None = None
-    password: str | None = None
-    type: int | None = None
-    bio: str | None = None
-    is_active: bool | None = None
-    ui_language: str | None = None
-    home_language: str | None = None
-    target_language: str | None = None
-    birthdate: NaiveDatetime | None = None
-    gender: str | None = None
-    calcom_link: str | None = None
-    last_survey: NaiveDatetime | None = None
-    availabilities: list[dict] | None = []
-    tutor_list: list[str] | None = []
-    my_tutor: str | None = None
-    my_slots: list[dict] | None = None
-
-    class Config:
-        from_attributes = True
-
-
 class ContactCreate(BaseModel):
     user_id: int
 
@@ -355,22 +277,3 @@ class SurveyResponseInfo(BaseModel):
     primary_language: str
     other_language: str
     education: str
-
-
-class Study(BaseModel):
-    id: int
-    title: str
-    description: str
-    start_date: NaiveDatetime
-    end_date: NaiveDatetime
-    chat_duration: int
-    users: list[User]
-    surveys: list[Survey]
-
-
-class StudyCreate(BaseModel):
-    title: str
-    description: str
-    start_date: NaiveDatetime
-    end_date: NaiveDatetime
-    chat_duration: int = 30 * 60
diff --git a/backend/app/schemas/studies.py b/backend/app/schemas/studies.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f22ab246b2880a9d5686271bc47a42921a84b8c
--- /dev/null
+++ b/backend/app/schemas/studies.py
@@ -0,0 +1,35 @@
+from pydantic import BaseModel, NaiveDatetime
+
+from schemas.users import User
+from schemas.tests import Test
+
+
+class StudyCreate(BaseModel):
+    title: str
+    description: str
+    start_date: NaiveDatetime
+    end_date: NaiveDatetime
+    nb_session: int = 8
+    consent_participation: str
+    consent_privacy: str
+    consent_rights: str
+    consent_study_data: str
+
+    user_ids: list[int] = []
+    test_ids: list[int] = []
+
+
+class Study(BaseModel):
+    id: int
+    title: str
+    description: str
+    start_date: NaiveDatetime
+    end_date: NaiveDatetime
+    nb_session: int = 8
+    consent_participation: str
+    consent_privacy: str
+    consent_rights: str
+    consent_study_data: str
+
+    users: list[User] = []
+    tests: list[Test] = []
diff --git a/backend/app/schemas/tests.py b/backend/app/schemas/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb49e00e88e488519c47bffbdc2de6908aa8fdb5
--- /dev/null
+++ b/backend/app/schemas/tests.py
@@ -0,0 +1,168 @@
+from typing_extensions import Self
+from pydantic import BaseModel, model_validator
+
+
+class TestTaskQuestionQCMCreate(BaseModel):
+    correct: int
+    option1: str | None = None
+    option2: str | None = None
+    option3: str | None = None
+    option4: str | None = None
+    option5: str | None = None
+    option6: str | None = None
+    option7: str | None = None
+    option8: str | None = None
+
+
+class TestTaskQuestionCreate(BaseModel):
+    # TODO remove
+    id: int | None = None
+    question: str | None = None
+
+    question_qcm: TestTaskQuestionQCMCreate | None = None
+
+
+class TestTaskGroupQuestionAdd(BaseModel):
+    question_id: int
+    group_id: int
+
+
+class TestTaskGroupAdd(BaseModel):
+    group_id: int
+    task_id: int
+
+
+class TestTaskQuestionQCM(BaseModel):
+    correct: int
+    options: list[str]
+
+    model_config = {"from_attributes": True}
+
+    @model_validator(mode="before")
+    @classmethod
+    def extract_options(cls, data):
+        if hasattr(data, "__dict__"):
+            result = {"correct": getattr(data, "correct", None), "options": []}
+
+            for i in range(1, 9):
+                option_value = getattr(data, f"option{i}", None)
+                if option_value is not None and option_value != "":
+                    result["options"].append(option_value)
+
+            return result
+        return data
+
+
+class TestTaskQuestion(BaseModel):
+    id: int
+    question: str | None = None
+    question_qcm: TestTaskQuestionQCM | None = None
+
+
+class TestTaskGroupCreate(BaseModel):
+    # TODO remove
+    id: int | None = None
+    title: str
+    demo: bool = False
+    randomize: bool = True
+
+
+class TestTypingCreate(BaseModel):
+    explanations: str
+    text: str
+    repeat: int | None = None
+    duration: int | None = None
+
+
+class TestTaskGroup(TestTaskGroupCreate):
+    # id: int
+    questions: list[TestTaskQuestion] = []
+
+
+class TestTaskCreate(BaseModel):
+    groups: list[TestTaskGroup] = []
+
+
+class TestTask(TestTaskCreate):
+    pass
+
+
+class TestCreate(BaseModel):
+    # TODO remove
+    id: int | None = None
+    title: str
+    test_typing: TestTypingCreate | None = None
+    test_task: TestTaskCreate | None = None
+
+    @model_validator(mode="after")
+    def check_test_type(self) -> Self:
+        if self.test_typing is None and self.test_task is None:
+            raise ValueError("TypingTest or TaskTest must be provided")
+        if self.test_typing is not None and self.test_task is not None:
+            raise ValueError(
+                "TypingTest and TaskTest cannot be provided at the same time"
+            )
+        return self
+
+
+class TestTyping(TestTypingCreate):
+    pass
+
+
+class Test(BaseModel):
+    id: int
+    title: str
+    test_typing: TestTyping | None = None
+    test_task: TestTask | None = None
+
+
+class TestTaskEntryQCMCreate(BaseModel):
+    selected_id: int
+
+
+class TestTaskEntryGapfillCreate(BaseModel):
+    text: str
+
+
+class TestTaskEntryCreate(BaseModel):
+    test_group_id: int
+    test_question_id: int
+    response_time: float
+
+    entry_task_qcm: TestTaskEntryQCMCreate | None = None
+    entry_task_gapfill: TestTaskEntryGapfillCreate | None = None
+
+    @model_validator(mode="after")
+    def check_entry_type(self) -> Self:
+        if self.entry_task_qcm is None and self.entry_task_gapfill is None:
+            raise ValueError("QCM or Gapfill must be provided")
+        if self.entry_task_qcm is not None and self.entry_task_gapfill is not None:
+            raise ValueError("QCM and Gapfill cannot be provided at the same time")
+        return self
+
+
+class TestTypingEntryCreate(BaseModel):
+    position: int
+    downtime: int
+    uptime: int
+    key_code: int
+    key_value: str
+
+
+class TestEntryCreate(BaseModel):
+    code: str
+    rid: str | None = None
+    user_id: int | None = None
+    test_id: int
+    study_id: int
+
+    entry_task: TestTaskEntryCreate | None = None
+    entry_typing: TestTypingEntryCreate | None = None
+
+    @model_validator(mode="after")
+    def check_entry_type(self) -> Self:
+        if self.entry_task is None and self.entry_typing is None:
+            raise ValueError("Task or Typing must be provided")
+        if self.entry_task is not None and self.entry_typing is not None:
+            raise ValueError("Task and Typing cannot be provided at the same time")
+        return self
diff --git a/backend/app/schemas/users.py b/backend/app/schemas/users.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff07d5df38b7d8f29aa0ecbaf068250eb941e728
--- /dev/null
+++ b/backend/app/schemas/users.py
@@ -0,0 +1,83 @@
+from pydantic import BaseModel, NaiveDatetime
+
+from models import UserType
+
+
+class User(BaseModel):
+    id: int
+    email: str
+    nickname: str
+    type: int
+    bio: str | None
+    is_active: bool
+    ui_language: str | None
+    home_language: str | None
+    target_language: str | None
+    birthdate: NaiveDatetime | None
+    gender: str | None = None
+    calcom_link: str | None
+    last_survey: NaiveDatetime | None = None
+    availabilities: list[dict] | None = []
+    tutor_list: list[str] | None = []
+    my_tutor: str | None = None
+    my_slots: list[dict] | None = []
+
+    class Config:
+        from_attributes = True
+
+    def to_dict(self):
+        return {
+            "id": self.id,
+            "email": self.email,
+            "nickname": self.nickname,
+            "type": self.type,
+            "availability": self.availability,
+            "is_active": self.is_active,
+            "ui_language": self.ui_language,
+            "home_language": self.home_language,
+            "target_language": self.target_language,
+            "birthdate": self.birthdate.isoformat() if self.birthdate else None,
+        }
+
+
+class UserCreate(BaseModel):
+    email: str
+    nickname: str | None = None
+    password: str
+    type: int = UserType.STUDENT.value
+    bio: str | None = None
+    is_active: bool = True
+    ui_language: str | None = None
+    home_language: str | None = None
+    target_language: str | None = None
+    birthdate: NaiveDatetime | None = None
+    gender: str | None = None
+    calcom_link: str | None = None
+    last_survey: NaiveDatetime | None = None
+    availabilities: list[dict] | None = []
+    tutor_list: list[str] | None = []
+    my_tutor: str | None = None
+    my_slots: list[dict] | None = []
+
+
+class UserUpdate(BaseModel):
+    email: str | None = None
+    nickname: str | None = None
+    password: str | None = None
+    type: int | None = None
+    bio: str | None = None
+    is_active: bool | None = None
+    ui_language: str | None = None
+    home_language: str | None = None
+    target_language: str | None = None
+    birthdate: NaiveDatetime | None = None
+    gender: str | None = None
+    calcom_link: str | None = None
+    last_survey: NaiveDatetime | None = None
+    availabilities: list[dict] | None = []
+    tutor_list: list[str] | None = []
+    my_tutor: str | None = None
+    my_slots: list[dict] | None = None
+
+    class Config:
+        from_attributes = True
diff --git a/backend/app/test_main.py b/backend/app/test_main.py
deleted file mode 100644
index dc5a19d6060b60bbe4a08aa65da4a85d85668f45..0000000000000000000000000000000000000000
--- a/backend/app/test_main.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from fastapi.testclient import TestClient
-import datetime
-
-from .main import app
-import config
-
-client = TestClient(app)
-
-
-def test_read_main():
-    response = client.get("/health")
-    assert response.status_code == 204
-
-
-def test_webhook_create():
-    response = client.post(
-        "/api/v1/webhooks/sessions",
-        headers={"X-Cal-Signature-256": config.CALCOM_SECRET},
-        json={
-            "triggerEvent": "BOOKING_CREATED",
-            "createdAt": "2023-05-24T09:30:00.538Z",
-            "payload": {
-                "type": "60min",
-                "title": "60min between Pro Example and John Doe",
-                "description": "",
-                "additionalNotes": "",
-                "customInputs": {},
-                "startTime": (
-                    datetime.datetime.now() + datetime.timedelta(days=1, hours=1)
-                ).isoformat(),
-                "endTime": (
-                    datetime.datetime.now() + datetime.timedelta(days=1, hours=2)
-                ).isoformat(),
-                "organizer": {
-                    "id": 5,
-                    "name": "Pro Example",
-                    "email": "pro@example.com",
-                    "username": "pro",
-                    "timeZone": "Asia/Kolkata",
-                    "language": {"locale": "en"},
-                    "timeFormat": "h:mma",
-                },
-                "responses": {
-                    "name": {"label": "your_name", "value": "John Doe"},
-                    "email": {
-                        "label": "email_address",
-                        "value": "john.doe@example.com",
-                    },
-                    "location": {
-                        "label": "location",
-                        "value": {"optionValue": "", "value": "inPerson"},
-                    },
-                    "notes": {"label": "additional_notes"},
-                    "guests": {"label": "additional_guests"},
-                    "rescheduleReason": {"label": "reschedule_reason"},
-                },
-                "userFieldsResponses": {},
-                "attendees": [
-                    {
-                        "email": "admin@admin.tld",
-                        "name": "John Doe",
-                        "timeZone": "Asia/Kolkata",
-                        "language": {"locale": "en"},
-                    }
-                ],
-                "location": "Calcom HQ",
-                "destinationCalendar": {
-                    "id": 10,
-                    "integration": "apple_calendar",
-                    "externalId": "https://caldav.icloud.com/1234567/calendars/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/",
-                    "userId": 5,
-                    "eventTypeId": None,
-                    "credentialId": 1,
-                },
-                "hideCalendarNotes": False,
-                "requiresConfirmation": None,
-                "eventTypeId": 7,
-                "seatsShowAttendees": True,
-                "seatsPerTimeSlot": None,
-                "uid": "bFJeNb2uX8ANpT3JL5EfXw",
-                "appsStatus": [
-                    {
-                        "appName": "Apple Calendar",
-                        "type": "apple_calendar",
-                        "success": 1,
-                        "failures": 0,
-                        "errors": [],
-                        "warnings": [],
-                    }
-                ],
-                "eventTitle": "60min",
-                "eventDescription": "",
-                "price": 0,
-                "currency": "usd",
-                "length": 60,
-                "bookingId": 91,
-                "metadata": {},
-                "status": "ACCEPTED",
-            },
-        },
-    )
-
-    assert response.status_code == 202, (
-        response.status_code,
-        response.content.decode("utf-8"),
-    )
diff --git a/backend/app/utils.py b/backend/app/utils.py
index 95b76652da50e7f938614a3228ece2dcc50947d2..09d7c58860a2f80d73cd5dd4146a65e26568652c 100644
--- a/backend/app/utils.py
+++ b/backend/app/utils.py
@@ -1,4 +1,5 @@
 import datetime
+import re
 
 
 def check_user_level(user, required_level):
@@ -9,3 +10,11 @@ def check_user_level(user, required_level):
 
 def datetime_aware():
     return datetime.datetime.now().astimezone(datetime.timezone.utc)
+
+
+def extract_text_between_angle_bracket(text):
+    pattern = r"<(.*?)>"
+    match = re.search(pattern, text)
+    if match:
+        return match.group(1)
+    return None
diff --git a/docs/db-diagrams/studies-and-tests.pdf b/docs/db-diagrams/studies-and-tests.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..1ef06afd4a22fba231883afff96a2ec8d8a697e2
Binary files /dev/null and b/docs/db-diagrams/studies-and-tests.pdf differ
diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json
index efeb4f3b90bbda30c30b074d4a1dfd77a5fc7f42..21fa5fefe0ba1190b940583e2e76b77ecd78614a 100644
--- a/frontend/src/lang/fr.json
+++ b/frontend/src/lang/fr.json
@@ -345,7 +345,8 @@
 		"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette étude ? Cette action est irréversible.",
 		"startDate": "Date de début",
 		"endDate": "Date de fin",
-		"chatDuration": "Durée des sessions (en secondes)",
+		"chatDuration": "Durée des sessions (en minutes)",
+		"nbSession": "Nombre de sessions",
 		"updated": "Étude mise à jour avec succès",
 		"noChanges": "Aucune modification",
 		"updateError": "Erreur lors de la mise à jour de l'étude",
@@ -364,7 +365,35 @@
 		"addUserError": "Erreur lors de l'ajout de l'utilisateur",
 		"addUserSuccess": "Utilisateur ajouté à l'étude",
 		"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette étude ? Cette action est irréversible.",
-		"createTitle": "Créer une nouvelle étude"
+		"createTitle": "Créer une nouvelle étude",
+		"editTitle": "Modification de l'étude",
+		"typingTest": "Activer le test de frappe",
+		"hasToLoggin": "Exiger que les participants soient inscrits et connectés pour passer le test",
+		"andOr": "et/ou",
+		"typingTestDuration": "Durée (en secondes)",
+		"typingTestRepetition": "Nombre de fois à répéter",
+		"typingTestText": "Texte",
+		"typingTestInfoNote": "Si aucune durée n'est fournis le mode \"plus vite que possible\" sera activé.",
+		"consentParticipation": "Si vous acceptez de participer, vous serez invité·e à participer à des sessions de tutorat en ligne avec un tuteur de langue étrangère. Vous serez également invité à remplir des questionnaires avant et après les sessions de tutorat. Les sessions de tutorat seront enregistrées pour analyse ultérieure.Nous vous demandons de prévoir de réaliser un minimum de 8 sessions d'une heure de tutorat (donc 8 heures au total), au cours d'une période de 1 à 3 mois. Vous pouvez bien sûr en réaliser plus si vous le souhaitez. Vous pouvez cependant arrêter de participer à l'étude à tout moment.",
+		"consentPrivacy": "Les données collectées (par exemple, les transcriptions des conversations, les résultats de tests, les mesures de frappe, les informations sur les participants comme l'age ou le genre) seront traitées de manière confidentielle et anonyme. Elles seront conservées après leur anonymisation intégrale et ne pourront être utilisées qu'à des fins scientifiques ou pédagogiques. Elles pourront éventuellement être partagées avec d'autres chercheurs ou enseignants, mais toujours dans ce cadre strictement de recherche ou d'enseignement.",
+		"consentRights": "Votre participation à cette étude est volontaire. Vous pouvez à tout moment décider de ne plus participer à l'étude sans avoir à vous justifier. Vous pouvez également demander à ce que vos données soient supprimées à tout moment. Si vous avez des questions ou des préoccupations concernant cette étude, vous pouvez contacter le responsable de l'étude.",
+		"consentStudyData": "Informations sur l'étude.",
+		"tab": {
+			"study": "Étude",
+			"code": "Code",
+			"tests": "Tests",
+			"end": "Fin"
+		},
+		"complete": "Merci pour votre participation !",
+		"score": {
+			"title": "Résultat moyen: ",
+			"loading": "Calcul en cours...",
+			"error": "Erreur lors du calcul du score"
+		}
+	},
+	"tests": {
+		"taskTests": "Tests de langue",
+		"typingTests": "Tests de frappe"
 	},
 	"button": {
 		"create": "Créer",
@@ -462,7 +491,10 @@
 			"users": "Utilisateurs",
 			"description": "Description",
 			"email": "E-mail",
-			"toggle": "Participants"
+			"toggle": "Participants",
+			"groups": "groupes",
+			"questions": "questions",
+			"tests": "tests"
 		}
 	},
 	"inputs": {
diff --git a/frontend/src/lib/api/studies.ts b/frontend/src/lib/api/studies.ts
index 7eff0b9115e82fc9001f425aa743c6244664d056..c4fa24394e3286a155f673d2dd6777a38fc57b46 100644
--- a/frontend/src/lib/api/studies.ts
+++ b/frontend/src/lib/api/studies.ts
@@ -26,7 +26,13 @@ export async function createStudyAPI(
 	description: string,
 	startDate: Date,
 	endDate: Date,
-	chatDuration: number
+	nbSession: number,
+	test_ids: number[],
+	consentParticipation: string,
+	consentPrivacy: string,
+	consentRights: string,
+	consentStudyData: string,
+	user_ids: number[]
 ): Promise<number | null> {
 	const response = await fetch('/api/studies', {
 		method: 'POST',
@@ -36,7 +42,13 @@ export async function createStudyAPI(
 			description,
 			start_date: formatToUTCDate(startDate),
 			end_date: formatToUTCDate(endDate),
-			chat_duration: chatDuration
+			nb_session: nbSession,
+			test_ids,
+			consent_participation: consentParticipation,
+			consent_privacy: consentPrivacy,
+			consent_rights: consentRights,
+			consent_study_data: consentStudyData,
+			user_ids
 		})
 	});
 	if (!response.ok) return null;
diff --git a/frontend/src/lib/api/survey.ts b/frontend/src/lib/api/survey.ts
index def03af31e4412db56808402ff31e530fa0d5287..cebdca2986c9f9ceaa59e571820896aec0a555ad 100644
--- a/frontend/src/lib/api/survey.ts
+++ b/frontend/src/lib/api/survey.ts
@@ -1,7 +1,13 @@
 import type { fetchType } from '$lib/utils/types';
 
+export async function getSurveysAPI(fetch: fetchType) {
+	const response = await fetch('/api/tests');
+	if (!response.ok) return null;
+	return await response.json();
+}
+
 export async function getSurveyAPI(fetch: fetchType, survey_id: number) {
-	const response = await fetch(`/api/surveys/${survey_id}`);
+	const response = await fetch(`/api/tests/${survey_id}`);
 	if (!response.ok) return null;
 
 	return await response.json();
@@ -19,7 +25,7 @@ export async function sendSurveyResponseAPI(
 	response_time: number,
 	text: string = ''
 ) {
-	const response = await fetch(`/api/surveys/responses`, {
+	const response = await fetch(`/api/tests/responses`, {
 		method: 'POST',
 		headers: { 'Content-Type': 'application/json' },
 		body: JSON.stringify({
@@ -39,7 +45,7 @@ export async function sendSurveyResponseAPI(
 }
 
 export async function getSurveyScoreAPI(fetch: fetchType, survey_id: number, sid: string) {
-	const response = await fetch(`/api/surveys/${survey_id}/score/${sid}`);
+	const response = await fetch(`/api/tests/${survey_id}/score/${sid}`);
 	if (!response.ok) return null;
 
 	return await response.json();
@@ -55,7 +61,7 @@ export async function sendSurveyResponseInfoAPI(
 	other_language: string,
 	education: string
 ) {
-	const response = await fetch(`/api/surveys/info/${survey_id}`, {
+	const response = await fetch(`/api/tests/info/${survey_id}`, {
 		method: 'POST',
 		headers: { 'Content-Type': 'application/json' },
 		body: JSON.stringify({
diff --git a/frontend/src/lib/api/tests.ts b/frontend/src/lib/api/tests.ts
index f8c69cd708513399375e482981efbff69743cbcd..54a3fe1f30bfb9e834da7419f01cf39db36a4af0 100644
--- a/frontend/src/lib/api/tests.ts
+++ b/frontend/src/lib/api/tests.ts
@@ -1,11 +1,122 @@
 import type { fetchType } from '$lib/utils/types';
 
-export async function sendTestVocabularyAPI(data: any): Promise<boolean> {
-	const response = await fetch(`/api/tests/vocabulary`, {
+export async function sendTestEntryTaskQcmAPI(
+	fetch: fetchType,
+	code: string,
+	rid: string | null,
+	user_id: number | null,
+	test_id: number,
+	study_id: number,
+	test_group_id: number,
+	test_question_id: number,
+	response_time: number,
+	selected_id: number
+) {
+	const response = await fetch(`/api/tests/entries`, {
 		method: 'POST',
 		headers: { 'Content-Type': 'application/json' },
-		body: JSON.stringify(data)
+		body: JSON.stringify({
+			code,
+			rid,
+			user_id,
+			test_id,
+			study_id,
+			entry_task: {
+				test_group_id,
+				test_question_id,
+				response_time,
+				entry_task_qcm: {
+					selected_id
+				}
+			}
+		})
 	});
 
 	return response.ok;
 }
+
+export async function sendTestEntryTaskGapfillAPI(
+	fetch: fetchType,
+	code: string,
+	rid: string | null,
+	user_id: number | null,
+	test_id: number,
+	test_group_id: number,
+	study_id: number,
+	test_question_id: number,
+	response_time: number,
+	text: string
+) {
+	const response = await fetch(`/api/tests/entries`, {
+		method: 'POST',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			code,
+			rid,
+			user_id,
+			test_id,
+			study_id,
+			entry_task: {
+				test_group_id,
+				test_question_id,
+				response_time,
+				entry_task_gapfill: {
+					text
+				}
+			}
+		})
+	});
+
+	return response.ok;
+}
+
+export async function sendTestEntryTypingAPI(
+	fetch: fetchType,
+	code: string,
+	rid: string | null,
+	user_id: number | null,
+	test_id: number,
+	study_id: number,
+	position: number,
+	downtime: number,
+	uptime: number,
+	key_code: number,
+	key_value: string
+) {
+	const response = await fetch(`/api/tests/entries`, {
+		method: 'POST',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			code,
+			rid,
+			user_id,
+			test_id,
+			study_id,
+			entry_typing: {
+				position,
+				downtime,
+				uptime,
+				key_code,
+				key_value
+			}
+		})
+	});
+
+	return response.ok;
+}
+
+export async function getTestEntriesScoreAPI(
+	fetch: fetchType,
+	rid: string
+): Promise<number | null> {
+	const response = await fetch(`/api/tests/entries/score/${rid}`);
+
+	if (!response.ok) return null;
+	const scoreText = await response.text();
+
+	const score = parseFloat(scoreText);
+
+	if (isNaN(score)) return null;
+
+	return score;
+}
diff --git a/frontend/src/lib/components/studies/Draggable.svelte b/frontend/src/lib/components/studies/Draggable.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..95fc21e0e99ec7800650decb359e9ce76a15658b
--- /dev/null
+++ b/frontend/src/lib/components/studies/Draggable.svelte
@@ -0,0 +1,110 @@
+<script lang="ts">
+	import { t } from '$lib/services/i18n';
+	import { TestTask, TestTyping } from '$lib/types/tests';
+
+	let { items = $bindable(), name } = $props();
+	let draggedIndex: number | null = $state(null);
+	let overIndex: number | null = $state(null);
+
+	const handleDragStart = (index: number) => {
+		draggedIndex = index;
+	};
+
+	const handleDragOver = (index: number, event: DragEvent) => {
+		event.preventDefault();
+		overIndex = index;
+	};
+
+	const handleDrop = (index: number) => {
+		if (draggedIndex !== null && draggedIndex !== index) {
+			const reordered = [...items];
+			const [removed] = reordered.splice(draggedIndex, 1);
+			reordered.splice(index, 0, removed);
+			items = reordered;
+		}
+		draggedIndex = null;
+		overIndex = null;
+	};
+
+	const handleDragEnd = () => {
+		draggedIndex = null;
+		overIndex = null;
+	};
+
+	const deleteItem = (index: number) => {
+		items = items.filter((_, i) => i !== index);
+	};
+</script>
+
+<ul class="w-full">
+	{#each items as item}
+		<input type="hidden" {name} value={item.id} />
+	{/each}
+	{#each items as item, index}
+		<li
+			class="p-3 bg-gray-200 border rounded-md select-none
+        transition-transform ease-out duration-200 flex mb-2
+        {index === draggedIndex ? 'opacity-50 bg-gray-300' : ''}
+        {index === overIndex ? 'border-dashed border-2 border-blue-500' : ''}"
+		>
+			<div class="w-full">
+				{#if item instanceof TestTask}
+					{item.title} ({item.groups.length}
+					{$t('utils.words.groups')}, {item.numQuestions}
+					{$t('utils.words.questions')})
+				{:else if item instanceof TestTyping}
+					{item.title}
+				{/if}
+			</div>
+			<div
+				class="ml-4 flex flex-col gap-1 cursor-grab"
+				draggable="true"
+				ondragstart={() => handleDragStart(index)}
+				ondragover={(e) => handleDragOver(index, e)}
+				ondrop={() => handleDrop(index)}
+				ondragend={handleDragEnd}
+				role="button"
+				tabindex="0"
+			>
+				<div class="flex gap-1">
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+				</div>
+				<div class="flex gap-1">
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+				</div>
+				<div class="flex gap-1">
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+					<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
+				</div>
+			</div>
+			<div>
+				<button
+					type="button"
+					class="ml-4 p-2 bg-red-500 text-white rounded-md hover:bg-red-600"
+					onclick={(e) => {
+						e.preventDefault();
+						deleteItem(index);
+					}}
+					aria-label="Delete"
+				>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						class="h-4 w-4"
+						fill="none"
+						viewBox="0 0 24 24"
+						stroke="currentColor"
+					>
+						<path
+							stroke-linecap="round"
+							stroke-linejoin="round"
+							stroke-width="2"
+							d="M6 18L18 6M6 6l12 12"
+						/>
+					</svg>
+				</button>
+			</div>
+		</li>
+	{/each}
+</ul>
diff --git a/frontend/src/lib/components/studies/StudyForm.svelte b/frontend/src/lib/components/studies/StudyForm.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..316dc64f87ac0f894b74e155c9119aef7edf8ce7
--- /dev/null
+++ b/frontend/src/lib/components/studies/StudyForm.svelte
@@ -0,0 +1,322 @@
+<script lang="ts">
+	import DateInput from '$lib/components/utils/dateInput.svelte';
+	import Draggable from './Draggable.svelte';
+	import autosize from 'svelte-autosize';
+	import { toastWarning, toastAlert } from '$lib/utils/toasts';
+	import { getUserByEmailAPI } from '$lib/api/users';
+	import { Icon, MagnifyingGlass } from 'svelte-hero-icons';
+	import { t } from '$lib/services/i18n';
+	import User from '$lib/types/user';
+	import type Study from '$lib/types/study';
+	import { onMount } from 'svelte';
+	import { TestTask, TestTyping, type Test } from '$lib/types/tests';
+	import type SurveyTypingSvelte from '$lib/types/surveyTyping.svelte';
+
+	let {
+		study = $bindable(),
+		possibleTests,
+		mode,
+		data,
+		form
+	}: {
+		study: Study | null;
+		possibleTests: (Test | SurveyTypingSvelte)[];
+		mode: string; //"create" or "edit"
+		data: any;
+		form: any;
+	} = $props();
+
+	let hasToLoggin: boolean = $state(false);
+	let title = study ? study.title : '';
+	let description = study ? study.description : '';
+	let startDate = study ? study.startDate : new Date();
+	let endDate = study ? study.endDate : new Date();
+	let nbSession = study ? study.nbSession : 8;
+	let tests = $state(study ? [...study.tests] : []);
+	let consentParticipation =
+		form?.consentParticipation ??
+		(study ? study.consentParticipation : $t('studies.consentParticipation'));
+	let consentPrivacy =
+		form?.consentPrivacy ?? (study ? study.consentPrivacy : $t('studies.consentPrivacy'));
+	let consentRights =
+		form?.consentRights ?? (study ? study.consentRights : $t('studies.consentRights'));
+	let consentStudyData =
+		form?.consentStudyData ?? (study ? study.consentStudyData : $t('studies.consentStudyData'));
+
+	let newUsername: string = $state('');
+	let newUserModal = $state(false);
+	let selectedTest: Test | undefined = $state();
+	let users: User[] = $state(study?.users ?? []);
+
+	/**
+	 * Triggers the autosize update for all textarea elements in the document, to adjust the textarea
+	 * size based on the initial content.
+	 */
+	function triggerAutosize() {
+		const textareas = document.querySelectorAll('textarea');
+		textareas.forEach((textarea) => {
+			autosize.update(textarea);
+		});
+	}
+	onMount(() => {
+		triggerAutosize();
+	});
+
+	/**
+	 * Opens the participant search dialog to allow adding a new user.
+	 */
+	function addUser() {
+		newUserModal = true;
+	}
+
+	/**
+	 * Add the new participant to the user list if the email given is properly formated and is found
+	 * in the database.
+	 * If successful close the dialog to search participant.
+	 * @async
+	 */
+	async function searchUser() {
+		if (!newUsername || !newUsername.includes('@')) {
+			toastWarning($t('studies.invalidEmail'));
+			return;
+		}
+		const userData = await getUserByEmailAPI(fetch, newUsername);
+		if (!userData) {
+			toastWarning($t('studies.userNotFound'));
+			return;
+		}
+		const user = User.parse(userData);
+		if (!user) {
+			toastAlert($t('studies.userNotFound'));
+			return;
+		}
+		users = [...users, user];
+		newUsername = '';
+		newUserModal = false;
+	}
+
+	/**
+	 * Remove the user from the list of users.
+	 * @param user the user to be removed
+	 */
+	function removeUser(user: User) {
+		users = users.filter((u) => u.id !== user.id);
+	}
+
+	/**
+	 * Deletes the current study from the database and redirects to the admin studies page.
+	 * @async
+	 */
+	async function deleteStudy() {
+		if (!study) return;
+		await study?.delete();
+		window.location.href = '/admin/studies';
+	}
+</script>
+
+<div class="mx-auto w-full max-w-5xl px-4">
+	<h2 class="text-xl font-bold m-5 text-center">
+		{$t(mode === 'create' ? 'studies.createTitle' : 'studies.editTitle')}
+	</h2>
+	<!-- if error message to display -->
+	{#if form?.message}
+		<div class="alert alert-error mb-4">
+			{form.message}
+		</div>
+	{/if}
+	<form method="post">
+		<!-- Title & description -->
+		<label class="label" for="title">{$t('utils.words.title')} *</label>
+		<input class="input w-full" type="text" id="title" name="title" required bind:value={title} />
+		<label class="label" for="description">{$t('utils.words.description')}</label>
+		<textarea
+			use:autosize
+			class="input w-full max-h-52"
+			id="description"
+			name="description"
+			bind:value={description}
+		></textarea>
+
+		<!-- Dates -->
+		<label class="label" for="startDate">{$t('studies.startDate')} *</label>
+		<DateInput class="input w-full" id="startDate" name="startDate" date={startDate} required />
+		<label class="label" for="endDate">{$t('studies.endDate')} *</label>
+		<DateInput class="input w-full" id="endDate" name="endDate" date={endDate} required />
+
+		<!-- number of Sessions -->
+		<label class="label" for="nbSession">{$t('studies.nbSession')} *</label>
+		<input
+			class="input w-full"
+			type="number"
+			id="nbSession"
+			name="nbSession"
+			min="0"
+			bind:value={nbSession}
+			required
+		/>
+
+		<!-- Tests Section -->
+		<h3 class="py-2 px-1 capitalize">{$t('utils.words.tests')}</h3>
+		<Draggable bind:items={tests} name="tests[]" />
+		<div class="flex">
+			<select class="select select-bordered flex-grow" bind:value={selectedTest}>
+				<optgroup label={$t('tests.taskTests')}>
+					{#each possibleTests as test}
+						{#if test instanceof TestTask}
+							<option value={test}
+								>{test.title} - {test.groups.length} groups - {test.numQuestions} questions</option
+							>
+						{/if}
+					{/each}
+				</optgroup>
+				<optgroup label={$t('tests.typingTests')}>
+					{#each possibleTests as test}
+						{#if test instanceof TestTyping}
+							<option value={test}>{test.title}</option>
+						{/if}
+					{/each}
+				</optgroup>
+			</select>
+			<button
+				class="ml-2 button"
+				onclick={(e) => {
+					e.preventDefault();
+					if (selectedTest === undefined) return;
+					tests = [...tests, selectedTest];
+				}}
+			>
+				+
+			</button>
+		</div>
+
+		<!-- Login Requirement -->
+		<div class="flex items-center mt-2">
+			<label class="label flex-grow" for="typingTest">{$t('studies.hasToLoggin')}*</label>
+			<input
+				type="checkbox"
+				class="checkbox checkbox-primary size-8"
+				id="typingTest"
+				bind:checked={hasToLoggin}
+			/>
+		</div>
+
+		<!-- Users Section -->
+		<label class="label" for="users">{$t('utils.words.users')}</label>
+		<table class="table">
+			<thead>
+				<tr>
+					<td>{$t('users.category')}</td>
+					<td>{$t('users.nickname')}</td>
+					<td>{$t('users.email')}</td>
+					<td></td>
+				</tr>
+			</thead>
+			<tbody>
+				{#each users ?? [] as user (user.id)}
+					<tr>
+						<td>{$t('users.type.' + user.type)}</td>
+						<td>
+							{user.nickname}
+							<input type="hidden" name="users[]" value={user.id} />
+						</td>
+						<td>{user.email}</td>
+						<td>
+							<button
+								type="button"
+								class="btn btn-sm btn-error text-white"
+								onclick={() => removeUser(user)}
+							>
+								{$t('button.remove')}
+							</button>
+						</td>
+					</tr>
+				{/each}
+			</tbody>
+		</table>
+		<button type="button" class="btn btn-primary block mx-auto mt-3" onclick={addUser}>
+			{$t('studies.addUserButton')}
+		</button>
+
+		<!-- Consent Section -->
+		<h3 class="py-2 px-1">{$t('register.consent.title')}</h3>
+		<label class="label text-sm" for="consentParticipation"
+			>{$t('register.consent.participation')} *</label
+		>
+		<textarea
+			use:autosize
+			class="input w-full max-h-52"
+			id="consentParticipation"
+			name="consentParticipation"
+			value={consentParticipation}
+			required
+		></textarea>
+		<label class="label text-sm" for="consentPrivacy">{$t('register.consent.privacy')} *</label>
+		<textarea
+			use:autosize
+			class="input w-full max-h-52"
+			id="consentPrivacy"
+			name="consentPrivacy"
+			value={consentPrivacy}
+			required
+		></textarea>
+		<label class="label text-sm" for="consentRights">{$t('register.consent.rights')} *</label>
+		<textarea
+			use:autosize
+			class="input w-full max-h-52"
+			id="consentRights"
+			name="consentRights"
+			value={consentRights}
+			required
+		></textarea>
+		<label class="label text-sm" for="consentStudyData"
+			>{$t('register.consent.studyData.title')} *</label
+		>
+		<textarea
+			use:autosize
+			class="input w-full max-h-52"
+			id="consentStudyData"
+			name="consentStudyData"
+			value={consentStudyData}
+			required
+		></textarea>
+
+		<!-- submit, cancel and delete buttons -->
+		<div class="mt-4 mb-6">
+			<input type="hidden" name="studyId" value={study ? study.id : ''} />
+			<button type="submit" class="button">
+				{$t(mode === 'create' ? 'button.create' : 'button.update')}
+			</button>
+			<a class="btn btn-outline float-end ml-2" href="/admin/studies">
+				{$t('button.cancel')}
+			</a>
+			{#if mode === 'edit'}
+				<button
+					type="button"
+					class="btn btn-error btn-outline float-end"
+					onclick={() => confirm($t('studies.deleteConfirm')) && deleteStudy()}
+				>
+					{$t('button.delete')}
+				</button>
+			{/if}
+		</div>
+	</form>
+</div>
+<!-- Dialog for the research of participant to be added -->
+<dialog class="modal bg-black bg-opacity-50" open={newUserModal}>
+	<div class="modal-box">
+		<h2 class="text-xl font-bold mb-4">{$t('studies.newUser')}</h2>
+		<div class="w-full flex">
+			<input
+				type="text"
+				placeholder={$t('utils.words.email')}
+				bind:value={newUsername}
+				class="input flex-grow mr-2"
+				onkeypress={(e) => e.key === 'Enter' && searchUser()}
+			/>
+			<button class="button w-16" onclick={searchUser}>
+				<Icon src={MagnifyingGlass} />
+			</button>
+		</div>
+		<button class="btn float-end mt-4" onclick={() => (newUserModal = false)}>Close</button>
+	</div>
+</dialog>
diff --git a/frontend/src/lib/components/surveys/gapfill.svelte b/frontend/src/lib/components/surveys/gapfill.svelte
index 5dbe48ecf0200563efc9224fce85eac2af8358b5..292351023b7a6c9d39f9d6e44886cfcd3e644098 100644
--- a/frontend/src/lib/components/surveys/gapfill.svelte
+++ b/frontend/src/lib/components/surveys/gapfill.svelte
@@ -4,7 +4,7 @@
 	let content: string = '';
 </script>
 
-<span class="relative text-blue-500 font-mono tracking-widest px-1"
+<span class="relative text-blue-500 font-mono tracking-[0.2em] px-1"
 	><!--
 	--><input
 		class="absolute bg-transparent text-transparent w-full caret-blue-500 focus:outline-none focus:ring-0"
diff --git a/frontend/src/lib/components/tests/languageTest.svelte b/frontend/src/lib/components/tests/languageTest.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..cf0a24047631443460353ae909716724d8b253fd
--- /dev/null
+++ b/frontend/src/lib/components/tests/languageTest.svelte
@@ -0,0 +1,210 @@
+<script lang="ts">
+	import { sendTestEntryTaskGapfillAPI, sendTestEntryTaskQcmAPI } from '$lib/api/tests';
+	import { t } from '$lib/services/i18n';
+	import type { TestTask } from '$lib/types/tests';
+	import {
+		TestTaskQuestion,
+		TestTaskQuestionGapfill,
+		TestTaskQuestionQcm,
+		TestTaskQuestionQcmType
+	} from '$lib/types/testTaskQuestions';
+	import type User from '$lib/types/user';
+	import Gapfill from '../surveys/gapfill.svelte';
+
+	let {
+		languageTest,
+		user,
+		code,
+		rid,
+		study_id,
+		onFinish = () => {}
+	}: {
+		languageTest: TestTask;
+		user: User | null;
+		code: string | null;
+		rid: string | null;
+		study_id: number;
+		onFinish: Function;
+	} = $props();
+
+	function getSortedQuestions(questions: TestTaskQuestion[]) {
+		return questions.sort(() => Math.random() - 0.5);
+	}
+
+	let nAnswers = $state(1);
+
+	let startTime = new Date().getTime();
+
+	let currentGroupId = $state(0);
+	let currentGroup = $derived(languageTest.groups[currentGroupId]);
+
+	let questions = $derived(
+		currentGroup.randomize ? getSortedQuestions(currentGroup.questions) : currentGroup.questions
+	);
+
+	let currentQuestionId = $state(0);
+	let currentQuestion = $derived(questions[currentQuestionId]);
+	let currentQuestionParts = $derived(
+		currentQuestion instanceof TestTaskQuestionGapfill ? currentQuestion.parts : null
+	);
+
+	let soundPlayer: HTMLAudioElement | null = $state(null);
+
+	function setGroupId(id: number) {
+		currentGroupId = id;
+		if (currentGroup.id < 1100) {
+			setQuestionId(0);
+		}
+	}
+
+	function setQuestionId(id: number) {
+		currentQuestionId = id;
+		if (soundPlayer) soundPlayer.load();
+		nAnswers += 1;
+	}
+
+	async function nextGroup() {
+		if (currentGroupId < languageTest.groups.length - 1) {
+			setGroupId(currentGroupId + 1);
+			//special group id for end of survey questions
+		} else {
+			console.log('END');
+			onFinish();
+		}
+	}
+
+	async function sendGap() {
+		if (
+			!currentGroup.demo &&
+			currentQuestion instanceof TestTaskQuestionGapfill &&
+			currentQuestionParts
+		) {
+			const gapTexts = currentQuestionParts
+				.filter((part) => part.gap !== null)
+				.map((part) => part.gap)
+				.join('|');
+
+			if (
+				!(await sendTestEntryTaskGapfillAPI(
+					fetch,
+					code || user?.email || '',
+					rid,
+					user?.id || null,
+					languageTest.id,
+					study_id,
+					currentGroup.id,
+					questions[currentQuestionId].id,
+					(new Date().getTime() - startTime) / 1000,
+					gapTexts
+				))
+			) {
+				return;
+			}
+		}
+		if (currentQuestionId < questions.length - 1) {
+			setQuestionId(currentQuestionId + 1);
+			startTime = new Date().getTime();
+		} else {
+			nextGroup();
+		}
+	}
+
+	async function selectOption(option: number) {
+		if (!currentGroup.demo) {
+			if (
+				!(await sendTestEntryTaskQcmAPI(
+					fetch,
+					code || user?.email || '',
+					rid,
+					user?.id || null,
+					languageTest.id,
+					study_id,
+					currentGroup.id,
+					questions[currentQuestionId].id,
+					(new Date().getTime() - startTime) / 1000,
+					option
+				))
+			) {
+				return;
+			}
+		}
+		if (currentQuestionId < questions.length - 1) {
+			setQuestionId(currentQuestionId + 1);
+			startTime = new Date().getTime();
+		} else {
+			nextGroup();
+		}
+	}
+</script>
+
+<div class="text-center">{nAnswers}/{languageTest.numQuestions}</div>
+
+{#if currentQuestion instanceof TestTaskQuestionGapfill && currentQuestionParts}
+	<div class="mx-auto mt-16 center flex flex-col text-xl">
+		<div>
+			{#each currentQuestionParts as part (part)}
+				{#if part.gap !== null}
+					<Gapfill length={part.text.length} onInput={(text) => (part.gap = text)} />
+				{:else}
+					{part.text}
+				{/if}
+			{/each}
+		</div>
+		<button class="button mt-8" onclick={sendGap}>{$t('button.next')}</button>
+	</div>
+{:else if currentQuestion instanceof TestTaskQuestionQcm}
+	<div class="mx-auto mt-16 text-center">
+		{#if currentQuestion.type === TestTaskQuestionQcmType.text}
+			<pre class="text-center font-bold py-4 px-6 m-auto">{currentQuestion.value}</pre>
+		{:else if currentQuestion.type === TestTaskQuestionQcmType.image}
+			<img src={currentQuestion.value} alt="Question" />
+		{:else if currentQuestion.type === TestTaskQuestionQcmType.audio}
+			<audio bind:this={soundPlayer} controls autoplay class="rounded-lg mx-auto">
+				<source src={currentQuestion.value} type="audio/mpeg" />
+				Your browser does not support the audio element.
+			</audio>
+		{/if}
+	</div>
+
+	<div class="mt-16 w-max">
+		<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
+			{#each currentQuestion.optionsRandomized as option (option)}
+				<div
+					class="h-48 w-48 overflow-hidden rounded-lg border border-black"
+					onclick={() => selectOption(option.index)}
+					role="button"
+					onkeydown={() => selectOption(option.index)}
+					tabindex="0"
+				>
+					{#if option.type === TestTaskQuestionQcmType.text}
+						<span
+							class="flex items-center justify-center h-full w-full text-2xl transition-transform duration-200 ease-in-out transform hover:scale-105"
+						>
+							{option.value}
+						</span>
+					{:else if option.type === TestTaskQuestionQcmType.image}
+						<img
+							src={option.value}
+							alt="Option {option}"
+							class="object-cover h-full w-full transition-transform duration-200 ease-in-out transform hover:scale-105"
+						/>
+					{:else if option.type === TestTaskQuestionQcmType.audio}
+						<audio
+							controls
+							class="w-full"
+							onclick={(e) => {
+								e.preventDefault();
+								e.stopPropagation();
+							}}
+						>
+							<source src={option.value} type="audio/mpeg" />
+							Your browser does not support the audio element.
+						</audio>
+					{/if}
+				</div>
+			{/each}
+		</div>
+	</div>
+{:else}
+	Nop
+{/if}
diff --git a/frontend/src/lib/components/tests/typingbox.svelte b/frontend/src/lib/components/tests/typingbox.svelte
index b6e8de1129996159a89be22b015ad5914bb19b60..9bdd821c2dde54bb11606b522d05c8d1851b8a97 100644
--- a/frontend/src/lib/components/tests/typingbox.svelte
+++ b/frontend/src/lib/components/tests/typingbox.svelte
@@ -2,27 +2,33 @@
 	import { onMount } from 'svelte';
 	import { t } from '$lib/services/i18n';
 	import config from '$lib/config';
+	import type { TestTyping } from '$lib/types/tests';
+	import { sendTestEntryTypingAPI } from '$lib/api/tests';
+	import type User from '$lib/types/user';
 
-	export let initialDuration: number;
-	export let exerciceId: number;
-	export let explications: string;
-	export let text: string;
-	export let data: {
-		exerciceId: number;
-		position: number;
-		downtime: number;
-		uptime: number;
-		keyCode: number;
-		keyValue: string;
-	}[];
-	export let inProgress = false;
-	export let onFinish: Function;
-	let duration = initialDuration >= 0 ? initialDuration : 0;
+	const {
+		typingTest,
+		onFinish,
+		user,
+		code,
+		rid
+	}: {
+		typingTest: TestTyping;
+		onFinish: Function;
+		user: User | null;
+		code: string | null;
+		rid: string | null;
+	} = $props();
+
+	let duration = $state(typingTest.initialDuration);
 	let lastInput = '';
-	let input = '';
+	let input = $state('');
 	let textArea: HTMLTextAreaElement;
 	let startTime = new Date().getTime();
-	let isDone = false;
+	let isDone = $state(false);
+	let inProgress = $state(false);
+
+	let currentPressings: { [key: string]: number } = {};
 
 	onMount(async () => {
 		textArea.focus();
@@ -32,8 +38,8 @@
 		inProgress = true;
 		startTime = new Date().getTime();
 		const interval = setInterval(() => {
-			duration += initialDuration >= 0 ? -1 : 1;
-			if ((duration <= 0 && initialDuration > 0) || !inProgress) {
+			duration += typingTest.durationStep;
+			if ((duration <= 0 && typingTest.durationDirection) || !inProgress) {
 				clearInterval(interval);
 				inProgress = false;
 				isDone = true;
@@ -41,16 +47,41 @@
 			}
 		}, 1000);
 	}
+
+	async function sendTyping(
+		position: number,
+		downtime: number,
+		uptime: number,
+		key_code: number,
+		key_value: string
+	) {
+		if (
+			!(await sendTestEntryTypingAPI(
+				fetch,
+				code || user?.email || '',
+				rid,
+				user?.id || null,
+				typingTest.id,
+				position,
+				downtime,
+				uptime,
+				key_code,
+				key_value
+			))
+		) {
+			return;
+		}
+	}
 </script>
 
 <div class=" w-full max-w-5xl m-auto mt-8">
-	<div>{explications}</div>
+	<div>{typingTest.explainations}</div>
 	<div class="flex justify-between my-2 p-2">
 		<button
 			class="button"
-			on:click={() => {
+			onclick={() => {
 				input = '';
-				duration = initialDuration >= 0 ? initialDuration : 0;
+				duration = typingTest.initialDuration;
 				start();
 			}}
 			disabled={inProgress}
@@ -65,10 +96,10 @@
 		{#each config.SPECIAL_CHARS as char (char)}
 			<button
 				class="flex-grow"
-				on:click={() => {
+				onclick={() => {
 					input += char;
 				}}
-				on:mousedown={(e) => e.preventDefault()}
+				onmousedown={(e) => e.preventDefault()}
 			>
 				{char}
 			</button>
@@ -82,7 +113,7 @@
 		<div class="font-mono p-4 break-words">
 			<span class="text-inherit p-0 m-0 whitespace-pre-wrap break-words"
 				><!--
-			-->{#each text.slice(0, input.length) as char, i}
+			-->{#each typingTest.textRepeated.slice(0, input.length) as char, i}
 					<span
 						class="text-gray-800 p-0 m-0"
 						class:text-red-500={char !== input[i]}
@@ -92,9 +123,9 @@
 					--></span
 			><span
 				class="text-gray-400 p-0 m-0 underline decoration-2 underline-offset-6 decoration-black whitespace-pre-wrap"
-				>{text[input.length] ?? ''}</span
+				>{typingTest.textRepeated[input.length] ?? ''}</span
 			><span class="text-gray-400 p-0 m-0 whitespace-pre-wrap"
-				>{text.slice(input.length + 1) ?? ''}</span
+				>{typingTest.textRepeated.slice(input.length + 1) ?? ''}</span
 			>
 		</div>
 		<textarea
@@ -102,31 +133,35 @@
 			bind:this={textArea}
 			spellcheck="false"
 			disabled={isDone}
-			on:keyup={() => {
-				if (inProgress) {
-					data[data.length - 1].uptime = new Date().getTime() - startTime;
+			onkeyup={(e) => {
+				if (e.keyCode in currentPressings) {
+					sendTyping(
+						input.length,
+						currentPressings[e.keyCode],
+						new Date().getTime() - startTime,
+						e.keyCode,
+						e.key
+					);
+					delete currentPressings[e.keyCode];
 				}
 			}}
-			on:keydown={(e) => {
+			onkeydown={(e) => {
 				if (
 					!inProgress &&
-					((duration > 0 && initialDuration > 0) || (duration >= 0 && initialDuration < 0))
+					((duration > 0 && typingTest.durationDirection) ||
+						(duration >= 0 && typingTest.duration <= 0))
 				) {
 					start();
 				}
 				if (inProgress) {
 					lastInput = input;
 
-					data.push({
-						exerciceId,
-						position: input.length,
-						downtime: new Date().getTime() - startTime,
-						uptime: 0,
-						keyCode: e.keyCode,
-						keyValue: e.key
-					});
+					currentPressings[e.keyCode] = new Date().getTime() - startTime;
 
-					if (input === text || input.split('\n').length >= text.split('\n').length + 1) {
+					if (
+						input === typingTest.textRepeated ||
+						input.split('\n').length >= typingTest.textRepeated.split('\n').length + 1
+					) {
 						inProgress = false;
 					}
 				} else {
diff --git a/frontend/src/lib/components/utils/dateInput.svelte b/frontend/src/lib/components/utils/dateInput.svelte
index 8a3cd51631fb9c5b082c3fe9b72a4dca24f3afcd..b2746ab06e710387459be4b0283d15f6d980df2c 100644
--- a/frontend/src/lib/components/utils/dateInput.svelte
+++ b/frontend/src/lib/components/utils/dateInput.svelte
@@ -5,19 +5,35 @@ source: https://svelte.dev/playground/dc963bbead384b69aad17824149d6d27?version=3
 <script lang="ts">
 	import dayjs from 'dayjs';
 
-	export let format = 'YYYY-MM-DD';
-	export let date: Date | null;
-	let class_: string | undefined;
-	export { class_ as class };
-	export let id: string | undefined;
+	let {
+		format = 'YYYY-MM-DD',
+		date,
+		id,
+		name,
+		required = false,
+		class: className
+	} = $props<{
+		format?: string;
+		date: Date | null;
+		id?: string;
+		name?: string;
+		required?: boolean;
+		class?: string;
+	}>();
 
-	let internal: string | null = null;
+	let internal = $state<string | null>(null);
 
-	const input = (x) => (internal = x ? dayjs(x).format(format) : null);
-	const output = (x) => (date = x ? dayjs(x, format).toDate() : null);
+	// Convert the input date to internal string format
+	let formattedDate = $derived(date ? dayjs(date).format(format) : null);
+	$effect(() => {
+		internal = formattedDate;
+	});
 
-	$: input(date);
-	$: output(internal);
+	// Convert internal string back to date
+	let parsedDate = $derived(internal ? dayjs(internal, format).toDate() : null);
+	$effect(() => {
+		date = parsedDate;
+	});
 </script>
 
-<input type="date" bind:value={internal} {id} class={class_} />
+<input type="date" bind:value={internal} {id} {name} class={className} {required} />
diff --git a/frontend/src/lib/types/study.ts b/frontend/src/lib/types/study.ts
index dd4353d3eed56d2028e2d38af8c98d7d79a03866..16f235ef857fd3db1a75ffdcf4d875af17f14fa8 100644
--- a/frontend/src/lib/types/study.ts
+++ b/frontend/src/lib/types/study.ts
@@ -9,6 +9,7 @@ import { parseToLocalDate } from '$lib/utils/date';
 import { toastAlert } from '$lib/utils/toasts';
 import type { fetchType } from '$lib/utils/types';
 import User from './user';
+import { Test } from './tests';
 
 export default class Study {
 	private _id: number;
@@ -16,8 +17,13 @@ export default class Study {
 	private _description: string;
 	private _startDate: Date;
 	private _endDate: Date;
-	private _chatDuration: number;
+	private _nbSession: number;
 	private _users: User[];
+	private _consentParticipation: string;
+	private _consentPrivacy: string;
+	private _consentRights: string;
+	private _consentStudyData: string;
+	private _tests: Test[];
 
 	private constructor(
 		id: number,
@@ -25,16 +31,26 @@ export default class Study {
 		description: string,
 		startDate: Date,
 		endDate: Date,
-		chatDuration: number,
-		users: User[]
+		nbSession: number,
+		users: User[],
+		consentParticipation: string,
+		consentPrivacy: string,
+		consentRights: string,
+		consentStudyData: string,
+		tests: Test[]
 	) {
 		this._id = id;
 		this._title = title;
 		this._description = description;
 		this._startDate = startDate;
 		this._endDate = endDate;
-		this._chatDuration = chatDuration;
+		this._nbSession = nbSession;
 		this._users = users;
+		this._consentParticipation = consentParticipation;
+		this._consentPrivacy = consentPrivacy;
+		this._consentRights = consentRights;
+		this._consentStudyData = consentStudyData;
+		this._tests = tests;
 	}
 
 	get id(): number {
@@ -45,50 +61,162 @@ export default class Study {
 		return this._title;
 	}
 
+	set title(value: string) {
+		this._title = value;
+	}
+
 	get description(): string {
 		return this._description;
 	}
 
+	set description(value: string) {
+		this._description = value;
+	}
+
 	get startDate(): Date {
 		return this._startDate;
 	}
 
+	set startDate(value: Date) {
+		this._startDate = value;
+	}
+
 	get endDate(): Date {
 		return this._endDate;
 	}
 
-	get chatDuration(): number {
-		return this._chatDuration;
+	set endDate(value: Date) {
+		this._endDate = value;
+	}
+
+	get nbSession(): number {
+		return this._nbSession;
+	}
+
+	set nbSession(value: number) {
+		this._nbSession = value;
 	}
 
 	get users(): User[] {
 		return this._users;
 	}
 
+	set users(value: User[]) {
+		this._users = value;
+	}
+
 	get numberOfUsers(): number {
 		return this._users.length;
 	}
 
+	get consentParticipation(): string {
+		return this._consentParticipation;
+	}
+
+	set consentParticipation(value: string) {
+		this._consentParticipation = value;
+	}
+
+	get consentPrivacy(): string {
+		return this._consentPrivacy;
+	}
+
+	set consentPrivacy(value: string) {
+		this._consentPrivacy = value;
+	}
+
+	get consentRights(): string {
+		return this._consentRights;
+	}
+
+	set consentRights(value: string) {
+		this._consentRights = value;
+	}
+
+	get consentStudyData(): string {
+		return this._consentStudyData;
+	}
+
+	set consentStudyData(value: string) {
+		this._consentStudyData = value;
+	}
+
+	get tests(): Test[] {
+		return this._tests;
+	}
+
+	set tests(value: Test[]) {
+		this._tests = value;
+	}
+
+	/**
+	 * Creates a new Study and saves it in the database.
+	 * @async
+	 * @returns a Study instance if successful, null otherwise
+	 */
 	static async create(
 		title: string,
 		description: string,
 		startDate: Date,
 		endDate: Date,
-		chatDuration: number,
+		nbSession: number,
+		consentParticipation: string,
+		consentPrivacy: string,
+		consentRights: string,
+		consentStudyData: string,
+		tests: Test[],
+		users: User[],
 		f: fetchType = fetch
 	): Promise<Study | null> {
-		const id = await createStudyAPI(f, title, description, startDate, endDate, chatDuration);
+		const id = await createStudyAPI(
+			f,
+			title,
+			description,
+			startDate,
+			endDate,
+			nbSession,
+			tests.map((t) => t.id),
+			consentParticipation,
+			consentPrivacy,
+			consentRights,
+			consentStudyData,
+			users.map((u) => u.id)
+		);
 
 		if (id) {
-			return new Study(id, title, description, startDate, endDate, chatDuration, []);
+			return new Study(
+				id,
+				title,
+				description,
+				startDate,
+				endDate,
+				nbSession,
+				users,
+				consentParticipation,
+				consentPrivacy,
+				consentRights,
+				consentStudyData,
+				tests
+			);
 		}
 		return null;
 	}
 
+	/**
+	 * Delete this study from the database
+	 * @async
+	 */
 	async delete(f: fetchType = fetch): Promise<void> {
 		await deleteStudyAPI(f, this._id);
 	}
 
+	/**
+	 * Update the study in the database with the new data.
+	 * @async
+	 * @param data  the updated fields
+	 * @param f
+	 * @return `true` if successful, `false` otherwise
+	 */
 	async patch(data: any, f: fetchType = fetch): Promise<boolean> {
 		const res = await patchStudyAPI(f, this._id, data);
 		if (res) {
@@ -96,12 +224,25 @@ export default class Study {
 			if (data.description) this._description = data.description;
 			if (data.start_date) this._startDate = parseToLocalDate(data.start_date);
 			if (data.end_date) this._endDate = parseToLocalDate(data.end_date);
-			if (data.chat_duration) this._chatDuration = data.chat_duration;
+			if (data.chat_duration) this._nbSession = data.chat_duration;
+			if (data.consent_participation) this._consentParticipation = data.consent_participation;
+			if (data.consent_privacy) this._consentPrivacy = data.consent_privacy;
+			if (data.consent_rights) this._consentRights = data.consent_rights;
+			if (data.consent_study_data) this._consentStudyData = data.consent_study_data;
+			if (data.tests) this._tests = data.tests;
 			return true;
 		}
 		return false;
 	}
 
+	/**
+	 * Remove the given user from this study in the database, if successful update the user list of
+	 * this study.
+	 * @async
+	 * @param user the user to remove
+	 * @param f
+	 * @return `true` if successful, `false` otherwise
+	 */
 	async removeUser(user: User, f: fetchType = fetch): Promise<boolean> {
 		const res = await removeUserToStudyAPI(f, this._id, user.id);
 		if (res) {
@@ -111,6 +252,14 @@ export default class Study {
 		return res;
 	}
 
+	/**
+	 * Add the given user from this study in the database, if successful update the user list of this
+	 * study.
+	 * @async
+	 * @param user the user to be added
+	 * @param f
+	 * @return `true` if successful, `false` otherwise
+	 */
 	async addUser(user: User, f: fetchType = fetch): Promise<boolean> {
 		const res = await addUserToStudyAPI(f, this._id, user.id);
 		if (res) {
@@ -120,6 +269,12 @@ export default class Study {
 		return res;
 	}
 
+	/**
+	 * Parses a JSON object into a Study instance.
+	 *
+	 * @param json the JSON object representing a study.
+	 * @returns a Study instance if successful, `null` otherwise
+	 */
 	static parse(json: any): Study | null {
 		if (json === null || json === undefined) {
 			toastAlert('Failed to parse study: json is null');
@@ -132,15 +287,27 @@ export default class Study {
 			json.description,
 			parseToLocalDate(json.start_date),
 			parseToLocalDate(json.end_date),
-			json.chat_duration,
+			json.nb_session,
+			[],
+			json.consent_participation,
+			json.consent_privacy,
+			json.consent_rights,
+			json.consent_study_data,
 			[]
 		);
 
 		study._users = User.parseAll(json.users);
+		study._tests = Test.parseAll(json.tests);
 
 		return study;
 	}
 
+	/**
+	 * Parses an array of json into an array of Study instances.
+	 *
+	 * @param json an array of json representing studies
+	 * @returns an array of Study instances. If parsing fails, returns an empty array.
+	 */
 	static parseAll(json: any): Study[] {
 		if (json === null || json === undefined) {
 			toastAlert('Failed to parse studies: json is null');
diff --git a/frontend/src/lib/types/survey.ts b/frontend/src/lib/types/survey.ts
index b1c28ed5dfc562e66628f9b8a4fc2f0c5819d39d..3408a8824a3acb70e9580577ba5e482c42fcd745 100644
--- a/frontend/src/lib/types/survey.ts
+++ b/frontend/src/lib/types/survey.ts
@@ -24,6 +24,10 @@ export default class Survey {
 		return this._groups;
 	}
 
+	get nQuestions(): number {
+		return this._groups.reduce((acc, group) => acc + group.questions.length, 0);
+	}
+
 	static parse(data: any): Survey | null {
 		if (data === null) {
 			toastAlert('Failed to parse survey data');
diff --git a/frontend/src/lib/types/surveyTyping.svelte.ts b/frontend/src/lib/types/surveyTyping.svelte.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d1a6cb7898c765182fbb51e2a380611775b7d88b
--- /dev/null
+++ b/frontend/src/lib/types/surveyTyping.svelte.ts
@@ -0,0 +1,6 @@
+export default class SurveyTypingSvelte {
+	name: string = 'Typing Test';
+	text: string = $state('');
+	repetition: number = $state(0);
+	duration: number = $state(0);
+}
diff --git a/frontend/src/lib/types/testTaskGroups.ts b/frontend/src/lib/types/testTaskGroups.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1b9eadfc13b0878cd10aeb8088a3fab0740611bd
--- /dev/null
+++ b/frontend/src/lib/types/testTaskGroups.ts
@@ -0,0 +1,64 @@
+import { toastAlert } from '$lib/utils/toasts';
+import { TestTaskQuestion } from './testTaskQuestions';
+
+export default class TestTaskGroup {
+	private _id: number;
+	private _title: string;
+	private _demo: boolean;
+	private _randomize: boolean;
+	private _questions: TestTaskQuestion[];
+
+	constructor(
+		id: number,
+		title: string,
+		demo: boolean,
+		randomize: boolean,
+		questions: TestTaskQuestion[]
+	) {
+		this._id = id;
+		this._title = title;
+		this._demo = demo;
+		this._randomize = randomize;
+		this._questions = questions;
+	}
+
+	get id(): number {
+		return this._id;
+	}
+
+	get title(): string {
+		return this._title;
+	}
+
+	get demo(): boolean {
+		return this._demo;
+	}
+
+	get randomize(): boolean {
+		return this._randomize;
+	}
+
+	get questions(): TestTaskQuestion[] {
+		return this._questions;
+	}
+
+	static parse(data: any): TestTaskGroup | null {
+		if (data === null) {
+			toastAlert('Failed to parse test task group data');
+			return null;
+		}
+		const questions = TestTaskQuestion.parseAll(data.questions);
+		return new TestTaskGroup(data.id, data.title, data.demo, data.randomize, questions);
+	}
+
+	static parseAll(data: any): TestTaskGroup[] {
+		if (data === null) {
+			toastAlert('Failed to parse test task group data');
+			return [];
+		}
+
+		return data
+			.map((group: any) => TestTaskGroup.parse(group))
+			.filter((group: TestTaskGroup | null): group is TestTaskGroup => group !== null);
+	}
+}
diff --git a/frontend/src/lib/types/testTaskQuestions.ts b/frontend/src/lib/types/testTaskQuestions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..afa027fa27cd563015c74d5b87edfbedf18b211c
--- /dev/null
+++ b/frontend/src/lib/types/testTaskQuestions.ts
@@ -0,0 +1,147 @@
+import { shuffle } from '$lib/utils/arrays';
+
+export abstract class TestTaskQuestion {
+	private _id: number;
+	private _question: string;
+
+	constructor(id: number, question: string) {
+		this._id = id;
+		this._question = question;
+	}
+
+	get id(): number {
+		return this._id;
+	}
+
+	get question(): string {
+		return this._question;
+	}
+
+	static parse(data: any): TestTaskQuestion | null {
+		if (data === null) {
+			return null;
+		}
+
+		if (data.question_qcm) {
+			return TestTaskQuestionQcm.parse(data);
+		}
+		return TestTaskQuestionGapfill.parse(data);
+	}
+
+	static parseAll(data: any): TestTaskQuestion[] {
+		if (data === null) {
+			return [];
+		}
+		return data
+			.map((question: any) => TestTaskQuestion.parse(question))
+			.filter(
+				(question: TestTaskQuestion | null): question is TestTaskQuestion => question !== null
+			);
+	}
+}
+
+export enum TestTaskQuestionQcmType {
+	image = 'image',
+	text = 'text',
+	audio = 'audio',
+	dropdown = 'dropdown',
+	radio = 'radio'
+}
+
+export class TestTaskQuestionQcm extends TestTaskQuestion {
+	private _options: string[];
+	private _correct: number;
+
+	constructor(id: number, question: string, options: string[], correct: number) {
+		super(id, question);
+		this._options = options;
+		this._correct = correct;
+	}
+
+	get options(): { type: string; value: string }[] {
+		return this._options.map((option) => {
+			const type = option.split(':')[0];
+			const value = option.split(':').slice(1).join(':');
+			return { type, value };
+		});
+	}
+
+	get optionsRandomized(): { type: string; value: string; index: number }[] {
+		let options = this.options.map((option, index) => ({ ...option, index }));
+		shuffle(options);
+		return options;
+	}
+
+	get correct(): number {
+		return this._correct;
+	}
+
+	get type(): TestTaskQuestionQcmType | null {
+		switch (this.question.split(':')[0]) {
+			case 'image':
+				return TestTaskQuestionQcmType.image;
+			case 'audio':
+				return TestTaskQuestionQcmType.audio;
+			case 'text':
+				return TestTaskQuestionQcmType.text;
+		}
+
+		return null;
+	}
+
+	get value(): string {
+		return this.question.split(':').slice(1).join(':');
+	}
+
+	static parse(data: any): TestTaskQuestionQcm | null {
+		if (data === null) {
+			return null;
+		}
+
+		return new TestTaskQuestionQcm(
+			data.id,
+			data.question,
+			data.question_qcm.options,
+			data.question_qcm.correct
+		);
+	}
+}
+
+export class TestTaskQuestionGapfill extends TestTaskQuestion {
+	get answer(): string {
+		const match = super.question.match(/<([^>]+)>/);
+		return match ? match[1] : '';
+	}
+
+	get length(): number {
+		return this.answer.length;
+	}
+
+	get blank(): string {
+		const question = super.question;
+		const match = question.match(/<([^>]+)>/);
+		if (!match) return question;
+		return question.replace(/<[^>]+>/, '_'.repeat(this.length));
+	}
+
+	get parts(): { text: string; gap: string | null }[] {
+		const gapText = this.question.split(':').slice(1).join(':');
+
+		const parts: { text: string; gap: string | null }[] = [];
+
+		for (let part of gapText.split(/(<.+?>)/g)) {
+			const isGap = part.startsWith('<') && part.endsWith('>');
+			const text = isGap ? part.slice(1, -1) : part;
+			parts.push({ text: text, gap: isGap ? '' : null });
+		}
+
+		return parts;
+	}
+
+	static parse(data: any): TestTaskQuestionGapfill | null {
+		if (data === null) {
+			return null;
+		}
+		return new TestTaskQuestionGapfill(data.id, data.question);
+	}
+}
diff --git a/frontend/src/lib/types/tests.ts b/frontend/src/lib/types/tests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e71ef0a1487686893530609b064a498fafabd51b
--- /dev/null
+++ b/frontend/src/lib/types/tests.ts
@@ -0,0 +1,149 @@
+import { toastAlert } from '$lib/utils/toasts';
+import TestTaskGroup from './testTaskGroups';
+
+export abstract class Test {
+	private _id: number;
+	private _title: string;
+
+	constructor(id: number, title: string) {
+		this._id = id;
+		this._title = title;
+	}
+
+	get id(): number {
+		return this._id;
+	}
+
+	get title(): string {
+		return this._title;
+	}
+
+	static parse(data: any): Test | null {
+		if (data === null) {
+			toastAlert('Failed to parse test data');
+			return null;
+		}
+
+		if (data.test_task) {
+			return TestTask.parse(data);
+		}
+
+		if (data.test_typing) {
+			return TestTyping.parse(data);
+		}
+
+		return null;
+	}
+
+	static parseAll(data: any): Test[] {
+		if (data === null) {
+			toastAlert('Failed to parse test data');
+			return [];
+		}
+
+		return data
+			.map((test: any) => Test.parse(test))
+			.filter((test: Test | null): test is Test => test !== null);
+	}
+}
+
+export class TestTask extends Test {
+	private _groups: TestTaskGroup[];
+
+	constructor(id: number, title: string, groups: TestTaskGroup[]) {
+		super(id, title);
+		this._groups = groups;
+	}
+
+	get groups(): TestTaskGroup[] {
+		return this._groups;
+	}
+
+	get numQuestions(): number {
+		return this._groups.reduce((acc, group) => acc + group.questions.length, 0);
+	}
+
+	static parse(data: any): TestTask | null {
+		if (data === null) {
+			toastAlert('Failed to parse test data');
+			return null;
+		}
+
+		const groups = TestTaskGroup.parseAll(data.test_task.groups);
+
+		return new TestTask(data.id, data.title, groups);
+	}
+}
+
+export class TestTyping extends Test {
+	private _text: string;
+	private _duration: number;
+	private _repeat: number;
+	private _explainations: string;
+
+	constructor(
+		id: number,
+		title: string,
+		text: string,
+		duration: number,
+		repeat: number,
+		explainations: string
+	) {
+		super(id, title);
+		this._text = text;
+		this._duration = duration;
+		this._repeat = repeat;
+		this._explainations = explainations;
+	}
+
+	get text(): string {
+		return this._text;
+	}
+
+	get duration(): number {
+		return this._duration;
+	}
+
+	get repeat(): number {
+		return this._repeat;
+	}
+
+	get explainations(): string {
+		return this._explainations;
+	}
+
+	get initialDuration(): number {
+		return this._duration > 0 ? this._duration : 0;
+	}
+
+	get durationDirection(): boolean {
+		return this._duration > 0;
+	}
+
+	get durationStep(): number {
+		return this.durationDirection ? -1 : 1;
+	}
+
+	get textRepeated(): string {
+		if (this._repeat === 0) {
+			return this._text.repeat(10 * this._duration);
+		} else {
+			return this._text.repeat(this._repeat);
+		}
+	}
+
+	static parse(data: any): TestTyping | null {
+		if (data === null) {
+			toastAlert('Failed to parse test data');
+			return null;
+		}
+		return new TestTyping(
+			data.id,
+			data.title,
+			data.test_typing.text,
+			data.test_typing.duration,
+			data.test_typing.repeat,
+			data.test_typing.explainations
+		);
+	}
+}
diff --git a/frontend/src/lib/utils/arrays.ts b/frontend/src/lib/utils/arrays.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e98fb56ee28763938577f8f4b7c58e15b98187ce
--- /dev/null
+++ b/frontend/src/lib/utils/arrays.ts
@@ -0,0 +1,12 @@
+//source: shuffle function code taken from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array/18650169#18650169
+export function shuffle(array: any[]) {
+	let currentIndex = array.length;
+	// While there remain elements to shuffle...
+	while (currentIndex != 0) {
+		// Pick a remaining element...
+		let randomIndex = Math.floor(Math.random() * currentIndex);
+		currentIndex--;
+		// And swap it with the current element.
+		[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
+	}
+}
diff --git a/frontend/src/lib/utils/toasts.ts b/frontend/src/lib/utils/toasts.ts
index a28dee8ed549b34bccd87d3a339f8968f64d54db..bade2a284030a55156efd9f7b898111c9c914057 100644
--- a/frontend/src/lib/utils/toasts.ts
+++ b/frontend/src/lib/utils/toasts.ts
@@ -1,6 +1,7 @@
 import { toast } from '@zerodevx/svelte-toast';
 
 export function toastAlert(title: string, subtitle: string = '', persistant: boolean = false) {
+	if (typeof window === 'undefined') return;
 	toast.push(`<strong>${title}</strong><br>${subtitle}`, {
 		theme: {
 			'--toastBackground': '#ff4d4f',
@@ -15,6 +16,7 @@ export function toastAlert(title: string, subtitle: string = '', persistant: boo
 }
 
 export function toastWarning(title: string, subtitle: string = '', persistant: boolean = false) {
+	if (typeof window === 'undefined') return;
 	toast.push(`<strong>${title}</strong><br>${subtitle}`, {
 		theme: {
 			'--toastBackground': '#faad14',
@@ -29,6 +31,7 @@ export function toastWarning(title: string, subtitle: string = '', persistant: b
 }
 
 export function toastSuccess(title: string, subtitle: string = '', persistant: boolean = false) {
+	if (typeof window === 'undefined') return;
 	toast.push(`<strong>${title}</strong><br>${subtitle}`, {
 		theme: {
 			'--toastBackground': '#52c41a',
diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts
index b3aecd87510c735f98e5cb872178617dbaeaccd9..a1671e66e89ee11a753fe73a1efbff66e06de10b 100644
--- a/frontend/src/routes/+layout.server.ts
+++ b/frontend/src/routes/+layout.server.ts
@@ -1,6 +1,13 @@
 import { type ServerLoad, redirect } from '@sveltejs/kit';
 
-const publicly_allowed = ['/login', '/register', '/tests', '/surveys', '/tutor/register'];
+const publicly_allowed = [
+	'/login',
+	'/register',
+	'/tests',
+	'/surveys',
+	'/tutor/register',
+	'/studies'
+];
 
 const isPublic = (path: string) => {
 	for (const allowed of publicly_allowed) {
diff --git a/frontend/src/routes/admin/sessions/+page.svelte b/frontend/src/routes/admin/sessions/+page.svelte
index dcf1ebbd398b606c3a3c95e8eb10ab06c2fd327d..c71531503f3a36c4085bb65bc50dc09bdee05987 100644
--- a/frontend/src/routes/admin/sessions/+page.svelte
+++ b/frontend/src/routes/admin/sessions/+page.svelte
@@ -16,21 +16,21 @@
 	<a
 		class="btn btn-primary btn-sm"
 		title="Download"
-		href={`${config.API_URL}/sessions/download/messages`}
+		href={`${config.API_URL}/v1/sessions/download/messages`}
 	>
 		{$t('session.downloadAllMessages')}
 	</a>
 	<a
 		class="btn btn-primary btn-sm"
 		title="Download"
-		href={`${config.API_URL}/sessions/download/metadata`}
+		href={`${config.API_URL}/v1/sessions/download/metadata`}
 	>
 		{$t('session.downloadAllMetadata')}
 	</a>
 	<a
 		class="btn btn-primary btn-sm"
 		title="Download"
-		href={`${config.API_URL}/sessions/download/feedbacks`}
+		href={`${config.API_URL}/v1/sessions/download/feedbacks`}
 	>
 		{$t('session.downloadAllFeedbacks')}
 	</a>
@@ -74,7 +74,7 @@
 					<a
 						class="btn btn-primary btn-sm"
 						title="Download"
-						href={`${config.API_URL}/sessions/${session.id}/download/messages`}
+						href={`${config.API_URL}/v1/sessions/${session.id}/download/messages`}
 					>
 						<Icon src={ArrowDownTray} size="16" />
 					</a>
diff --git a/frontend/src/routes/admin/studies/+page.svelte b/frontend/src/routes/admin/studies/+page.svelte
index 56920d9dfb3c47abf0c39f1b43d2c58585cf3e35..5ca59fd342866aab97f9da7936f1a9d4ee9d74a0 100644
--- a/frontend/src/routes/admin/studies/+page.svelte
+++ b/frontend/src/routes/admin/studies/+page.svelte
@@ -1,167 +1,14 @@
 <script lang="ts">
 	import { t } from '$lib/services/i18n';
-	import Study from '$lib/types/study';
-	import { displayDate, formatToUTCDate } from '$lib/utils/date';
-	import autosize from 'svelte-autosize';
-	import DateInput from '$lib/components/utils/dateInput.svelte';
-	import { toastAlert, toastSuccess, toastWarning } from '$lib/utils/toasts';
-	import User from '$lib/types/user';
-	import { Icon, MagnifyingGlass } from 'svelte-hero-icons';
-	import { getUserByEmailAPI } from '$lib/api/users';
+	import Study from '$lib/types/study.js';
+	import { displayDate } from '$lib/utils/date';
 	import type { PageData } from './$types';
+	import config from '$lib/config';
+	import { ArrowDownTray, Icon } from 'svelte-hero-icons';
 
 	const { data }: { data: PageData } = $props();
 
 	let studies: Study[] = $state(data.studies);
-	let selectedStudy: Study | null = $state(null);
-	let title: string | null = $state(null);
-	let description: string | null = $state(null);
-	let startDate: Date | null = $state(null);
-	let endDate: Date | null = $state(null);
-	let chatDuration: number | null = $state(null);
-	let typingTest: boolean = $state(true);
-
-	let studyCreate: boolean = $state(false);
-
-	function selectStudy(study: Study | null) {
-		selectedStudy = study;
-
-		title = study?.title ?? null;
-		description = study?.description ?? null;
-		startDate = study?.startDate ?? null;
-		endDate = study?.endDate ?? null;
-		chatDuration = study?.chatDuration ?? null;
-	}
-
-	async function studyUpdate() {
-		if (
-			selectedStudy === null ||
-			title === null ||
-			description === null ||
-			startDate === null ||
-			endDate === null ||
-			chatDuration === null ||
-			title === '' ||
-			description === '' ||
-			(title === selectedStudy.title &&
-				description === selectedStudy.description &&
-				startDate.getDay() === selectedStudy.startDate.getDay() &&
-				startDate.getMonth() === selectedStudy.startDate.getMonth() &&
-				startDate.getFullYear() === selectedStudy.startDate.getFullYear() &&
-				endDate.getDay() === selectedStudy.endDate.getDay() &&
-				endDate.getMonth() === selectedStudy.endDate.getMonth() &&
-				endDate.getFullYear() === selectedStudy.endDate.getFullYear() &&
-				chatDuration === selectedStudy.chatDuration)
-		) {
-			selectStudy(null);
-			toastSuccess($t('studies.noChanges'));
-			return;
-		}
-
-		const result = await selectedStudy.patch({
-			title,
-			description,
-			start_date: formatToUTCDate(startDate),
-			end_date: formatToUTCDate(endDate),
-			chatDuration
-		});
-
-		if (result) {
-			selectStudy(null);
-			toastSuccess($t('studies.updated'));
-		} else {
-			toastAlert($t('studies.updateError'));
-		}
-	}
-
-	async function createStudy() {
-		if (
-			title === null ||
-			description === null ||
-			startDate === null ||
-			endDate === null ||
-			chatDuration === null ||
-			title === '' ||
-			description === ''
-		) {
-			toastAlert($t('studies.createMissing'));
-			return;
-		}
-
-		const study = await Study.create(title, description, startDate, endDate, chatDuration);
-
-		if (study) {
-			toastSuccess($t('studies.created'));
-			studyCreate = false;
-			studies.push(study);
-		} else {
-			toastAlert($t('studies.createError'));
-		}
-	}
-
-	async function deleteStudy() {
-		if (!selectedStudy) return;
-
-		studies.splice(studies.indexOf(selectedStudy), 1);
-		selectedStudy?.delete();
-		selectStudy(null);
-	}
-
-	async function removeUser(user: User) {
-		if (selectedStudy === null) return;
-		if (!confirm($t('studies.removeUserConfirm'))) return;
-
-		const res = await selectedStudy.removeUser(user);
-
-		if (res) {
-			toastSuccess($t('studies.removeUserSuccess'));
-			selectStudy(null);
-		} else {
-			toastAlert($t('studies.removeUserError'));
-		}
-	}
-
-	let newUsername: string = $state('');
-	let newUserModal = $state(false);
-
-	async function addUser() {
-		if (selectedStudy === null) return;
-		newUserModal = true;
-	}
-
-	async function searchUser() {
-		if (selectedStudy === null) return;
-		if (!newUsername || !newUsername.includes('@')) {
-			toastWarning($t('studies.invalidEmail'));
-			return;
-		}
-
-		const userData = await getUserByEmailAPI(fetch, newUsername);
-
-		if (!userData) {
-			toastWarning($t('studies.userNotFound'));
-			return;
-		}
-
-		const user = User.parse(userData);
-
-		if (!user) {
-			toastAlert($t('studies.userNotFound'));
-			return;
-		}
-
-		const res = await selectedStudy.addUser(user);
-
-		if (!res) {
-			toastAlert($t('studies.addUserError'));
-			return;
-		}
-
-		newUsername = '';
-		newUserModal = false;
-		toastSuccess($t('studies.addUserSuccess'));
-		selectStudy(null);
-	}
 </script>
 
 <h1 class="text-xl font-bold m-5 text-center">{$t('header.admin.studies')}</h1>
@@ -177,138 +24,28 @@
 	</thead>
 	<tbody>
 		{#each studies as study (study.id)}
-			<tr class="hover:bg-gray-100 hover:cursor-pointer" onclick={() => selectStudy(study)}>
+			<tr
+				class="hover:bg-gray-100 hover:cursor-pointer"
+				onclick={() => (window.location.href = `/admin/studies/${study.id}`)}
+			>
 				<td>{study.id}</td>
 				<td>{displayDate(study.startDate)} - {displayDate(study.endDate)}</td>
 				<td>{study.title}</td>
 				<td>{study.numberOfUsers}</td>
+				<td
+					><a
+						class="btn btn-primary btn-sm"
+						title="Download"
+						href={`${config.API_URL}/v1/studies/${study.id}/download/surveys`}
+					>
+						<Icon src={ArrowDownTray} size="16" />
+					</a></td
+				>
 				<td></td>
 			</tr>
 		{/each}
 	</tbody>
 </table>
 <div class="mt-8 w-[64rem] mx-auto">
-	<button class="button" onclick={() => (studyCreate = true)}>{$t('studies.create')}</button>
+	<a class="button" href="/admin/studies/new">{$t('studies.create')}</a>
 </div>
-
-<dialog class="modal bg-black bg-opacity-50" open={selectedStudy != null}>
-	<div class="modal-box max-w-4xl">
-		<h2 class="text-xl font-bold m-5 text-center">{$t('studies.study')} #{selectedStudy?.id}</h2>
-		<form class="mb-4">
-			<label class="label" for="title">{$t('utils.words.title')}</label>
-			<input class="input w-full" type="text" id="title" bind:value={title} />
-			<label class="label" for="description">{$t('utils.words.description')}</label>
-			<textarea use:autosize class="input w-full max-h-52" id="title" bind:value={description}>
-			</textarea>
-			<label class="label" for="startDate">{$t('studies.startDate')}</label>
-			<DateInput class="input w-full" id="startDate" bind:date={startDate} />
-			<label class="label" for="endDate">{$t('studies.endDate')}</label>
-			<DateInput class="input w-full" id="endDate" bind:date={endDate} />
-			<label class="label" for="chatDuration">{$t('studies.chatDuration')}</label>
-			<input
-				class="input w-full"
-				type="number"
-				id="chatDuration"
-				bind:value={chatDuration}
-				min="0"
-			/>
-			<label class="label" for="users">{$t('utils.words.users')}</label>
-			<table class="table">
-				<thead>
-					<tr>
-						<td>#</td>
-						<td>{$t('users.category')}</td>
-						<td>{$t('users.nickname')}</td>
-						<td>{$t('users.email')}</td>
-						<td></td>
-					</tr>
-				</thead>
-				<tbody>
-					{#each selectedStudy?.users ?? [] as user (user.id)}
-						<tr>
-							<td>{user.id}</td>
-							<td>{$t('users.type.' + user.type)}</td>
-							<td>{user.nickname}</td>
-							<td>{user.email}</td>
-							<td>
-								<button class="btn btn-sm btn-error text-white" onclick={() => removeUser(user)}>
-									{$t('button.remove')}
-								</button>
-							</td>
-						</tr>
-					{/each}
-				</tbody>
-			</table>
-			<button class="btn btn-primary block mx-auto" onclick={addUser}>
-				{$t('studies.addUserButton')}
-			</button>
-		</form>
-		<div class="mt-4">
-			<button class="button" onclick={studyUpdate}>{$t('button.update')}</button>
-			<button class="btn btn-outline float-end ml-2" onclick={() => selectStudy(null)}>
-				{$t('button.cancel')}
-			</button>
-			<button
-				class="btn btn-error btn-outline float-end"
-				onclick={() => confirm($t('studies.deleteConfirm')) && deleteStudy()}
-			>
-				{$t('button.delete')}
-			</button>
-		</div>
-	</div>
-</dialog>
-
-<dialog class="modal bg-black bg-opacity-50" open={studyCreate}>
-	<div class="modal-box max-w-4xl">
-		<h2 class="text-xl font-bold m-5 text-center">{$t('studies.createTitle')}</h2>
-		<form>
-			<label class="label" for="title">{$t('utils.words.title')} *</label>
-			<input class="input w-full" type="text" id="title" bind:value={title} />
-			<label class="label" for="description">{$t('utils.words.description')} *</label>
-			<textarea use:autosize class="input w-full max-h-52" id="title" bind:value={description}>
-			</textarea>
-			<label class="label" for="startDate">{$t('studies.startDate')} *</label>
-			<DateInput class="input w-full" id="startDate" bind:date={startDate} />
-			<label class="label" for="endDate">{$t('studies.endDate')} *</label>
-			<DateInput class="input w-full" id="endDate" bind:date={endDate} />
-			<label class="label" for="chatDuration">{$t('studies.chatDuration')} *</label>
-			<input
-				class="input w-full"
-				type="number"
-				id="chatDuration"
-				bind:value={chatDuration}
-				min="0"
-			/>
-			<label class="label" for="typingTest">{$t('studies.typingTest')} *</label>
-			<input type="checkbox" class="input" id="typingTest" bind:checked={typingTest} />
-		</form>
-		<div class="mt-4">
-			<button class="button" onclick={createStudy}>{$t('button.create')}</button>
-			<button
-				class="btn btn-outline float-end ml-2"
-				onclick={() => (studyCreate = false && selectStudy(null))}
-			>
-				{$t('button.cancel')}
-			</button>
-		</div>
-	</div>
-</dialog>
-
-<dialog class="modal bg-black bg-opacity-50" open={newUserModal}>
-	<div class="modal-box">
-		<h2 class="text-xl font-bold mb-4">{$t('studies.newUser')}</h2>
-		<div class="w-full flex">
-			<input
-				type="text"
-				placeholder={$t('utils.words.email')}
-				bind:value={newUsername}
-				class="input flex-grow mr-2"
-				onkeypress={(e) => e.key === 'Enter' && searchUser()}
-			/>
-			<button class="button w-16" onclick={searchUser}>
-				<Icon src={MagnifyingGlass} />
-			</button>
-		</div>
-		<button class="btn float-end mt-4" onclick={() => (newUserModal = false)}>Close</button>
-	</div>
-</dialog>
diff --git a/frontend/src/routes/admin/studies/+page.ts b/frontend/src/routes/admin/studies/+page.ts
index f4043711bee65e230b7b3fee7fa244006d43629d..9d25fe3cd9b706b830df8d78a2c576a38dea53d8 100644
--- a/frontend/src/routes/admin/studies/+page.ts
+++ b/frontend/src/routes/admin/studies/+page.ts
@@ -1,5 +1,5 @@
 import { getStudiesAPI } from '$lib/api/studies';
-import Study from '$lib/types/study';
+import Study from '$lib/types/study.js';
 import { type Load } from '@sveltejs/kit';
 
 export const load: Load = async ({ fetch }) => {
diff --git a/frontend/src/routes/admin/studies/[id]/+page.server.ts b/frontend/src/routes/admin/studies/[id]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..389eb799a56778497e890e338fff1bdcbea7ea05
--- /dev/null
+++ b/frontend/src/routes/admin/studies/[id]/+page.server.ts
@@ -0,0 +1,78 @@
+import { patchStudyAPI } from '$lib/api/studies';
+import { redirect, type Actions } from '@sveltejs/kit';
+import { formatToUTCDate } from '$lib/utils/date';
+
+export const actions: Actions = {
+	default: async ({ request, fetch, params }) => {
+		const formData = await request.formData();
+		const studyId = params.id;
+
+		if (!studyId) return { message: 'Invalid study ID' };
+
+		const title = formData.get('title')?.toString();
+		const description = formData.get('description')?.toString() || '';
+		const startDateStr = formData.get('startDate')?.toString();
+		const endDateStr = formData.get('endDate')?.toString();
+		const nbSessionStr = formData.get('nbSession')?.toString();
+		const consentParticipation = formData.get('consentParticipation')?.toString();
+		const consentPrivacy = formData.get('consentPrivacy')?.toString();
+		const consentRights = formData.get('consentRights')?.toString();
+		const consentStudyData = formData.get('consentStudyData')?.toString();
+
+		if (
+			!title ||
+			!startDateStr ||
+			!endDateStr ||
+			!nbSessionStr ||
+			!consentParticipation ||
+			!consentPrivacy ||
+			!consentRights ||
+			!consentStudyData
+		) {
+			return { message: 'Invalid request' };
+		}
+
+		const startDate = new Date(startDateStr);
+		const endDate = new Date(endDateStr);
+		const nbSession = parseInt(nbSessionStr, 10);
+
+		if (isNaN(startDate.getTime()) || isNaN(endDate.getTime()) || isNaN(nbSession)) {
+			return { message: 'Invalid date or session amount' };
+		}
+
+		if (startDate.getTime() > endDate.getTime()) {
+			return { message: 'End time cannot be before start time' };
+		}
+
+		const test_ids = formData
+			.getAll('tests[]')
+			.map((test) => {
+				try {
+					return JSON.parse(test.toString());
+				} catch (e) {
+					return null;
+				}
+			})
+			.filter((test) => test !== null);
+
+		const user_ids = formData.getAll('users[]').map((user) => parseInt(user.toString(), 10));
+
+		const updated = await patchStudyAPI(fetch, parseInt(studyId, 10), {
+			title: title,
+			description: description,
+			start_date: formatToUTCDate(startDate),
+			end_date: formatToUTCDate(endDate),
+			nb_session: nbSession,
+			test_ids,
+			consent_participation: consentParticipation,
+			consent_privacy: consentPrivacy,
+			consent_rights: consentRights,
+			consent_study_data: consentStudyData,
+			user_ids
+		});
+
+		if (!updated) return { message: 'Failed to update study' };
+
+		return redirect(303, '/admin/studies');
+	}
+};
diff --git a/frontend/src/routes/admin/studies/[id]/+page.svelte b/frontend/src/routes/admin/studies/[id]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..4c588c7c70a9936bc319a24c105dc22cc3f771bb
--- /dev/null
+++ b/frontend/src/routes/admin/studies/[id]/+page.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+	import StudyForm from '$lib/components/studies/StudyForm.svelte';
+	import type { PageData, ActionData } from './$types';
+	import SurveyTypingSvelte from '$lib/types/surveyTyping.svelte';
+
+	let { data, form }: { data: PageData; form: ActionData } = $props();
+	let study = data.study;
+
+	let typing = $state(new SurveyTypingSvelte());
+	let possibleTests = [typing, ...data.tests];
+
+	let mode = 'edit';
+</script>
+
+{#if study}
+	<StudyForm {study} {possibleTests} {mode} {data} {form} />
+{:else}
+	<p>Study not found.</p>
+{/if}
diff --git a/frontend/src/routes/admin/studies/[id]/+page.ts b/frontend/src/routes/admin/studies/[id]/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba7b272dfcd9882cf95f841816e58e562ee35925
--- /dev/null
+++ b/frontend/src/routes/admin/studies/[id]/+page.ts
@@ -0,0 +1,22 @@
+import { getSurveysAPI } from '$lib/api/survey';
+import { type Load, redirect } from '@sveltejs/kit';
+import Study from '$lib/types/study.js';
+import { getStudyAPI } from '$lib/api/studies';
+import { Test } from '$lib/types/tests';
+
+export const load: Load = async ({ fetch, params }) => {
+	const id = Number(params.id);
+
+	const study = Study.parse(await getStudyAPI(fetch, id));
+
+	if (!study) {
+		redirect(303, '/admin/studies');
+	}
+
+	const tests = Test.parseAll(await getSurveysAPI(fetch));
+
+	return {
+		tests,
+		study
+	};
+};
diff --git a/frontend/src/routes/admin/studies/new/+page.server.ts b/frontend/src/routes/admin/studies/new/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be9f519471115f103810a1ca85ec801e4e697a52
--- /dev/null
+++ b/frontend/src/routes/admin/studies/new/+page.server.ts
@@ -0,0 +1,96 @@
+import { createStudyAPI } from '$lib/api/studies';
+import { redirect, type Actions } from '@sveltejs/kit';
+
+export const actions: Actions = {
+	default: async ({ request, fetch }) => {
+		const formData = await request.formData();
+
+		const title = formData.get('title')?.toString();
+		let description = formData.get('description')?.toString();
+		const startDateStr = formData.get('startDate')?.toString();
+		const endDateStr = formData.get('endDate')?.toString();
+		const nbSessionStr = formData.get('nbSession')?.toString();
+
+		const consentParticipation = formData.get('consentParticipation')?.toString();
+		const consentPrivacy = formData.get('consentPrivacy')?.toString();
+		const consentRights = formData.get('consentRights')?.toString();
+		const consentStudyData = formData.get('consentStudyData')?.toString();
+
+		if (
+			!title ||
+			!startDateStr ||
+			!endDateStr ||
+			!nbSessionStr ||
+			!consentParticipation ||
+			!consentPrivacy ||
+			!consentRights ||
+			!consentStudyData
+		) {
+			return {
+				message: 'Invalid request 1'
+			};
+		}
+
+		if (description === undefined) {
+			description = '';
+		}
+
+		const startDate = new Date(startDateStr);
+		const endDate = new Date(endDateStr);
+
+		if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+			return {
+				message: 'Invalid request 2'
+			};
+		}
+
+		const nbSession = parseInt(nbSessionStr, 10);
+		if (isNaN(nbSession)) {
+			return {
+				message: 'Invalid request 3'
+			};
+		}
+
+		if (startDate.getTime() > endDate.getTime()) {
+			return {
+				message: 'End time cannot be before start time'
+			};
+		}
+
+		const tests = formData
+			.getAll('tests[]')
+			.map((test) => {
+				try {
+					return JSON.parse(test.toString());
+				} catch (e) {
+					return null;
+				}
+			})
+			.filter((test) => test !== null);
+
+		const user_ids = formData.getAll('users[]').map((user) => parseInt(user.toString(), 10));
+
+		const id = await createStudyAPI(
+			fetch,
+			title,
+			description,
+			startDate,
+			endDate,
+			nbSession,
+			tests,
+			consentParticipation,
+			consentPrivacy,
+			consentRights,
+			consentStudyData,
+			user_ids
+		);
+
+		if (id === null) {
+			return {
+				message: 'Failed to create study'
+			};
+		}
+
+		return redirect(303, '/admin/studies');
+	}
+};
diff --git a/frontend/src/routes/admin/studies/new/+page.svelte b/frontend/src/routes/admin/studies/new/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..25a9047f1e89323956b56c217172e3a40ee7970a
--- /dev/null
+++ b/frontend/src/routes/admin/studies/new/+page.svelte
@@ -0,0 +1,13 @@
+<script lang="ts">
+	import StudyForm from '$lib/components/studies/StudyForm.svelte';
+	import type { PageData, ActionData } from './$types';
+	import SurveyTypingSvelte from '$lib/types/surveyTyping.svelte';
+
+	let { data, form }: { data: PageData; form: ActionData } = $props();
+	let study = null;
+	let typing = $state(new SurveyTypingSvelte());
+	let possibleTests = [typing, ...data.tests];
+	let mode = 'create';
+</script>
+
+<StudyForm {study} {possibleTests} {mode} {data} {form} />
diff --git a/frontend/src/routes/admin/studies/new/+page.ts b/frontend/src/routes/admin/studies/new/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..741a66fc934a2d2fe34cac63c9e3e47df0aac933
--- /dev/null
+++ b/frontend/src/routes/admin/studies/new/+page.ts
@@ -0,0 +1,11 @@
+import { getSurveysAPI } from '$lib/api/survey';
+import { Test } from '$lib/types/tests';
+import { type Load } from '@sveltejs/kit';
+
+export const load: Load = async ({ fetch }) => {
+	const tests = Test.parseAll(await getSurveysAPI(fetch));
+
+	return {
+		tests
+	};
+};
diff --git a/frontend/src/routes/register/[[studyId]]/+page.svelte b/frontend/src/routes/register/[[studyId]]/+page.svelte
index 85ae010c0a51d4ef4f19c8c564385d1410ec3b6b..171c6e782aa6c617579cf063da80b42a1bcad648 100644
--- a/frontend/src/routes/register/[[studyId]]/+page.svelte
+++ b/frontend/src/routes/register/[[studyId]]/+page.svelte
@@ -7,8 +7,8 @@
 	import Typingtest from '$lib/components/tests/typingtest.svelte';
 	import { browser } from '$app/environment';
 	import type { PageData } from './$types';
-	import type Study from '$lib/types/study';
 	import Consent from '$lib/components/surveys/consent.svelte';
+	import type Study from '$lib/types/study';
 
 	let { data, form }: { data: PageData; form: FormData } = $props();
 	let study: Study | undefined = $state(data.study);
diff --git a/frontend/src/routes/studies/[[id]]/+page.svelte b/frontend/src/routes/studies/[[id]]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..81ae73c2e4e51fbb8b6f2ee807f44f09f0aa85de
--- /dev/null
+++ b/frontend/src/routes/studies/[[id]]/+page.svelte
@@ -0,0 +1,222 @@
+<script lang="ts">
+	import type Study from '$lib/types/study';
+	import type { PageData } from './$types';
+	import { t } from '$lib/services/i18n';
+	import { displayDate } from '$lib/utils/date';
+	import { toastWarning } from '$lib/utils/toasts';
+	import { get } from 'svelte/store';
+	import LanguageTest from '$lib/components/tests/languageTest.svelte';
+	import { TestTask, TestTyping } from '$lib/types/tests';
+	import Typingbox from '$lib/components/tests/typingbox.svelte';
+	import { getTestEntriesScoreAPI } from '$lib/api/tests';
+
+	let { data, form }: { data: PageData; form: FormData } = $props();
+	let study: Study | undefined = $state(data.study);
+	let studies: Study[] | undefined = $state(data.studies);
+	let user = $state(data.user);
+	let rid =
+		Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+
+	let selectedStudy: Study | undefined = $state();
+
+	let current_step = $state(
+		(() => {
+			if (!study) return 0;
+			if (!user) return 1;
+			return 2;
+		})()
+	);
+
+	let code = $state('');
+
+	function checkCode() {
+		if (!code) {
+			toastWarning(get(t)('tests.invalidCode'));
+			return;
+		}
+		if (code.length < 3) {
+			toastWarning(get(t)('tests.invalidCode'));
+			return;
+		}
+
+		current_step += 1;
+	}
+</script>
+
+<div class="header mx-auto my-5">
+	<ul class="steps text-xs">
+		<li class="step" class:step-primary={current_step >= 0}>
+			{$t('studies.tab.study')}
+		</li>
+		<li class="step" class:step-primary={current_step >= 1}>
+			{$t('studies.tab.code')}
+		</li>
+		{#if study}
+			{#each study.tests as test, i (test.id)}
+				<li class="step" class:step-primary={current_step >= i + 2}>
+					{test.title}
+				</li>
+			{/each}
+		{:else}
+			<li class="step" data-content="...">
+				{$t('studies.tab.tests')}
+			</li>
+		{/if}
+		<li class="step" class:step-primary={study && current_step >= study.tests.length + 2}>
+			{$t('studies.tab.end')}
+		</li>
+	</ul>
+</div>
+
+<div class="max-w-screen-md min-w-max mx-auto p-5 h-full">
+	{#if current_step == 0}
+		<div class="form-control">
+			<label for="study" class="label">
+				<span class="label-text">{$t('register.study')}</span>
+				<span class="label-text-alt">{$t('register.study.note')}</span>
+			</label>
+			<select
+				class="select select-bordered"
+				id="study"
+				name="study"
+				required
+				disabled={!!study}
+				bind:value={selectedStudy}
+			>
+				{#if study}
+					<option selected value={study}>
+						{study.title} ({displayDate(study.startDate)} - {displayDate(study.endDate)})
+					</option>
+				{:else if studies}
+					<option disabled selected value="">{$t('register.study.placeholder')}</option>
+					{#each studies as s}
+						<option value={s}>
+							{s.title} ({displayDate(s.startDate)} - {displayDate(s.endDate)})
+						</option>
+					{/each}
+				{:else}
+					<option disabled></option>
+				{/if}
+			</select>
+		</div>
+		<div class="form-control">
+			<a
+				class="button mt-8"
+				class:btn-disabled={!selectedStudy}
+				href="/studies/{selectedStudy?.id}"
+				data-sveltekit-reload
+			>
+				{$t('button.continue')}
+			</a>
+		</div>
+	{:else if study}
+		{#if current_step == 1}
+			<div class="flex flex-col items-center min-h-screen">
+				<h2 class="mb-10 text-xl text-center">{study.title}</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}
+				/>
+				<button
+					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>
+			</div>
+		{:else if current_step < study.tests.length + 2}
+			{@const test = study.tests[current_step - 2]}
+			{#key test}
+				{#if test instanceof TestTask}
+					<LanguageTest
+						languageTest={test}
+						{user}
+						{code}
+						{rid}
+						study_id={study.id}
+						onFinish={() => current_step++}
+					/>
+				{:else if test instanceof TestTyping}
+					<div class="w-[1024px]">
+						<Typingbox
+							typingTest={test}
+							onFinish={() => {
+								setTimeout(() => {
+									current_step++;
+								}, 3000);
+							}}
+							{user}
+							{code}
+							{rid}
+						/>
+					</div>
+				{/if}
+			{/key}
+		{:else if current_step == study.tests.length + 2}
+			<div class="flex flex-col h-full">
+				<div class="flex-grow text-center mt-16">
+					<span>{$t('studies.complete')}</span>
+
+					<div class="mt-4">
+						{$t('studies.score.title')}
+						{#await getTestEntriesScoreAPI(fetch, rid)}
+							{$t('studies.score.loading')}
+						{:then score}
+							{#if score !== null}
+								{(score * 100).toFixed(0)}%
+							{:else}
+								{$t('studies.score.error')}
+							{/if}
+						{/await}
+					</div>
+				</div>
+
+				<dl class="text-sm">
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.study')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							{study.title}
+						</dd>
+					</div>
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.project')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							{$t('register.consent.studyData.projectD')}
+						</dd>
+					</div>
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.university')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							{$t('register.consent.studyData.universityD')}
+						</dd>
+					</div>
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.address')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							{$t('register.consent.studyData.addressD')}
+						</dd>
+					</div>
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.person')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							{$t('register.consent.studyData.personD')}
+						</dd>
+					</div>
+					<div class="sm:grid sm:grid-cols-3 sm:gap-4 mb-1">
+						<dt class="font-medium">{$t('register.consent.studyData.email')}</dt>
+						<dd class="text-gray-700 sm:col-span-2">
+							<a href="mailto:{$t('register.consent.studyData.emailD')}" class="link"
+								>{$t('register.consent.studyData.emailD')}</a
+							>
+						</dd>
+					</div>
+				</dl>
+			</div>
+		{/if}
+	{/if}
+</div>
diff --git a/frontend/src/routes/studies/[[id]]/+page.ts b/frontend/src/routes/studies/[[id]]/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d309a7c716bcd4427520c5467811f9d53d3f909b
--- /dev/null
+++ b/frontend/src/routes/studies/[[id]]/+page.ts
@@ -0,0 +1,25 @@
+import { getStudiesAPI, getStudyAPI } from '$lib/api/studies';
+import Study from '$lib/types/study.js';
+import type { Load } from '@sveltejs/kit';
+
+export const load: Load = async ({ fetch, params }) => {
+	const sStudyId: string | undefined = params.id;
+	if (sStudyId) {
+		const studyId = parseInt(sStudyId);
+		if (studyId) {
+			const study = Study.parse(await getStudyAPI(fetch, studyId));
+			if (study) {
+				return {
+					study
+				};
+			}
+		}
+	}
+
+	const studies = Study.parseAll(await getStudiesAPI(fetch));
+
+	return {
+		studyError: true,
+		studies
+	};
+};
diff --git a/frontend/src/routes/tests/[id]/+page.svelte b/frontend/src/routes/tests/[id]/+page.svelte
deleted file mode 100644
index a2c19dcb368bc36afa58d6a514b79c59d47b8970..0000000000000000000000000000000000000000
--- a/frontend/src/routes/tests/[id]/+page.svelte
+++ /dev/null
@@ -1,462 +0,0 @@
-<script lang="ts">
-	import { sendSurveyResponseAPI, sendSurveyResponseInfoAPI } from '$lib/api/survey';
-	import { getSurveyScoreAPI } from '$lib/api/survey';
-	import { t } from '$lib/services/i18n';
-	import { toastWarning } from '$lib/utils/toasts.js';
-	import { get } from 'svelte/store';
-	import type SurveyGroup from '$lib/types/surveyGroup';
-	import Gapfill from '$lib/components/surveys/gapfill.svelte';
-	import type { PageData } from './$types';
-	import Consent from '$lib/components/surveys/consent.svelte';
-	import Dropdown from '$lib/components/surveys/dropdown.svelte';
-	import config from '$lib/config';
-	import type User from '$lib/types/user';
-	import type Survey from '$lib/types/survey';
-
-	let { data }: { data: PageData } = $props();
-	let { user, survey }: { user: User | null; survey: Survey } = data;
-
-	let sid =
-		Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
-	let startTime = new Date().getTime();
-
-	function getSortedQuestions(group: SurveyGroup) {
-		return group.questions.sort(() => Math.random() - 0.5);
-	}
-
-	let step = $state(user ? 2 : 0);
-	let uid = $state(user?.id || null);
-	let code = $state('');
-	let subStep = $state(0);
-
-	let currentGroupId = $state(0);
-	survey.groups.sort((a, b) => {
-		if (a.demo === b.demo) {
-			return 0;
-		}
-		return a.demo ? -1 : 1;
-	});
-	let currentGroup = $derived(survey.groups[currentGroupId]);
-	let questionsRandomized = $derived(getSortedQuestions(currentGroup));
-	let currentQuestionId = $state(0);
-	let currentQuestion = $derived(questionsRandomized[currentQuestionId]);
-	let type = $derived(currentQuestion?.question.split(':')[0]);
-	let value = $derived(currentQuestion?.question.split(':').slice(1).join(':'));
-	let gaps = $derived(type === 'gap' ? gapParts(currentQuestion.question) : null);
-	let soundPlayer: HTMLAudioElement;
-	let displayQuestionOptions: string[] = $derived(
-		(() => {
-			let d = [...(currentQuestion?.options ?? [])];
-			shuffle(d);
-			return d;
-		})()
-	);
-	let finalScore: number | null = $state(null);
-	let selectedOption: string;
-	let endSurveyAnswers: { [key: string]: any } = {};
-
-	//source: shuffle function code taken from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array/18650169#18650169
-	function shuffle(array: string[]) {
-		let currentIndex = array.length;
-		// While there remain elements to shuffle...
-		while (currentIndex != 0) {
-			// Pick a remaining element...
-			let randomIndex = Math.floor(Math.random() * currentIndex);
-			currentIndex--;
-			// And swap it with the current element.
-			[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
-		}
-	}
-
-	function setGroupId(id: number) {
-		currentGroupId = id;
-		if (currentGroup.id < 1100) {
-			setQuestionId(0);
-		}
-	}
-
-	function setQuestionId(id: number) {
-		currentQuestionId = id;
-		if (soundPlayer) soundPlayer.load();
-	}
-
-	async function selectOption(option: string) {
-		if (!currentGroup.demo) {
-			if (
-				!(await sendSurveyResponseAPI(
-					fetch,
-					code,
-					sid,
-					uid,
-					survey.id,
-					currentGroup.id,
-					questionsRandomized[currentQuestionId].id,
-					currentQuestion.options.findIndex((o: string) => o === option) + 1,
-					(new Date().getTime() - startTime) / 1000
-				))
-			) {
-				return;
-			}
-		}
-		if (currentQuestionId < questionsRandomized.length - 1) {
-			setQuestionId(currentQuestionId + 1);
-			startTime = new Date().getTime();
-		} else {
-			nextGroup();
-		}
-	}
-
-	async function sendGap() {
-		if (!gaps) return;
-		if (!currentGroup.demo) {
-			const gapTexts = gaps
-				.filter((part) => part.gap !== null)
-				.map((part) => part.gap)
-				.join('|');
-
-			if (
-				!(await sendSurveyResponseAPI(
-					fetch,
-					code,
-					sid,
-					uid,
-					survey.id,
-					currentGroup.id,
-					questionsRandomized[currentQuestionId].id,
-					-1,
-					(new Date().getTime() - startTime) / 1000,
-					gapTexts
-				))
-			) {
-				return;
-			}
-		}
-		if (currentQuestionId < questionsRandomized.length - 1) {
-			setQuestionId(currentQuestionId + 1);
-			startTime = new Date().getTime();
-		} else {
-			nextGroup();
-		}
-	}
-
-	async function nextGroup() {
-		if (currentGroupId < survey.groups.length - 1) {
-			setGroupId(currentGroupId + 1);
-			//special group id for end of survey questions
-			if (currentGroup.id >= 1100) {
-				const scoreData = await getSurveyScoreAPI(fetch, survey.id, sid);
-				if (scoreData) {
-					finalScore = scoreData.score;
-				}
-				step += user ? 2 : 1;
-				return;
-			}
-		} else {
-			const scoreData = await getSurveyScoreAPI(fetch, survey.id, sid);
-			if (scoreData) {
-				finalScore = scoreData.score;
-			}
-			step += 2;
-		}
-	}
-
-	function checkCode() {
-		if (!code) {
-			toastWarning(get(t)('surveys.invalidCode'));
-			return;
-		}
-		if (code.length < 3) {
-			toastWarning(get(t)('surveys.invalidCode'));
-			return;
-		}
-
-		step += 1;
-	}
-
-	function gapParts(question: string): { text: string; gap: string | null }[] {
-		if (!question.startsWith('gap:')) return [];
-
-		const gapText = question.split(':').slice(1).join(':');
-
-		const parts: { text: string; gap: string | null }[] = [];
-
-		for (let part of gapText.split(/(<.+?>)/g)) {
-			const isGap = part.startsWith('<') && part.endsWith('>');
-			const text = isGap ? part.slice(1, -1) : part;
-			parts.push({ text: text, gap: isGap ? '' : null });
-		}
-
-		return parts;
-	}
-
-	async function selectAnswer(selection: string, option: string) {
-		endSurveyAnswers[selection] = option;
-		subStep += 1;
-		if (subStep == 5) {
-			await sendSurveyResponseInfoAPI(
-				fetch,
-				survey.id,
-				sid,
-				endSurveyAnswers.birthYear,
-				endSurveyAnswers.gender,
-				endSurveyAnswers.primaryLanguage,
-				endSurveyAnswers.other_language,
-				endSurveyAnswers.education
-			);
-			step += 1;
-		}
-		selectedOption = '';
-		return;
-	}
-</script>
-
-{#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">{survey.title}</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}
-		/>
-		<button
-			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>
-	</div>
-{:else if step == 1}
-	<div class="max-w-screen-md mx-auto p-5">
-		<Consent
-			introText={$t('surveys.consent.intro')}
-			participation={$t('surveys.consent.participation')}
-			participationD={$t('surveys.consent.participationD')}
-			privacy={$t('surveys.consent.privacy')}
-			privacyD={$t('surveys.consent.privacyD')}
-			rights={$t('surveys.consent.rights')}
-		/>
-		<div class="form-control">
-			<button class="button mt-4" onclick={() => step++}>
-				{$t('surveys.consent.ok')}
-			</button>
-		</div>
-	</div>
-{:else if step == 2}
-	{#if currentGroup.demo}
-		<div class="mx-auto mt-10 text-center">
-			<p class="text-center font-bold text-xl m-auto">{$t('surveys.example')}</p>
-		</div>
-	{/if}
-	{#if type == 'gap' && gaps}
-		<div class="mx-auto mt-16 center flex flex-col">
-			<div>
-				{#each gaps as part (part)}
-					{#if part.gap !== null}
-						<Gapfill length={part.text.length} onInput={(text) => (part.gap = text)} />
-					{:else}
-						{part.text}
-					{/if}
-				{/each}
-			</div>
-			<button class="button mt-8" onclick={sendGap}>{$t('button.next')}</button>
-		</div>
-	{:else}
-		<div class="mx-auto mt-16 text-center">
-			{#if type == 'text'}
-				<pre class="text-center font-bold py-4 px-6 m-auto">{value}</pre>
-			{:else if type == 'image'}
-				<img src={value} alt="Question" />
-			{:else if type == 'audio'}
-				<audio bind:this={soundPlayer} controls autoplay class="rounded-lg mx-auto">
-					<source src={value} type="audio/mpeg" />
-					Your browser does not support the audio element.
-				</audio>
-			{/if}
-		</div>
-
-		<div class="mx-auto mt-16">
-			<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
-				{#each displayQuestionOptions as option, i (option)}
-					{@const type = option.split(':')[0]}
-					{#if type == 'dropdown'}
-						{@const value = option.split(':')[1].split(', ')}
-						<select
-							class="select select-bordered !ml-0"
-							id="dropdown"
-							name="dropdown"
-							bind:value={displayQuestionOptions[i]}
-							onchange={() => selectOption(option)}
-							required
-						>
-							{#each value as op}
-								<option value={op}>{op}</option>
-							{/each}
-						</select>
-					{:else if type == 'radio'}
-						{@const value = option.split(':')[1].split(', ')}
-						{#each value as op}
-							<label class="radio-label">
-								<input
-									type="radio"
-									name="dropdown"
-									value={op}
-									onchange={() => selectOption(op)}
-									required
-									class="radio-button"
-								/>
-								{op}
-							</label>
-						{/each}
-					{:else}
-						{@const value = option.split(':').slice(1).join(':')}
-						<div
-							class="h-48 w-48 overflow-hidden rounded-lg border border-black"
-							onclick={() => selectOption(option)}
-							role="button"
-							onkeydown={() => selectOption(option)}
-							tabindex="0"
-						>
-							{#if type === 'text'}
-								<span
-									class="flex items-center justify-center h-full w-full text-2xl transition-transform duration-200 ease-in-out transform hover:scale-105"
-								>
-									{value}
-								</span>
-							{:else if type === 'image'}
-								<img
-									src={value}
-									alt="Option {option}"
-									class="object-cover h-full w-full transition-transform duration-200 ease-in-out transform hover:scale-105"
-								/>
-							{:else if type == 'audio'}
-								<audio
-									controls
-									class="w-full"
-									onclick={(e) => {
-										e.preventDefault();
-										e.stopPropagation();
-									}}
-								>
-									<source src={value} type="audio/mpeg" />
-									Your browser does not support the audio element.
-								</audio>
-							{/if}
-						</div>
-					{/if}
-				{/each}
-			</div>
-		</div>
-	{/if}
-{:else if step === 3}
-	{#if currentGroup.id === 1100}
-		{@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') }
-		]}
-		{#if subStep === 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={() => selectAnswer('birthYear', selectedOption)}
-				></Dropdown>
-			</div>
-		{:else if subStep === 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={() => selectAnswer('gender', value)}
-								required
-								class="radio-button"
-							/>
-							<span>{label}</span>
-						</label>
-					{/each}
-				</div>
-			</div>
-		{:else if subStep === 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={() => selectAnswer('primaryLanguage', selectedOption)}
-				></Dropdown>
-			</div>
-		{:else if subStep === 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={() => selectAnswer('other_language', selectedOption)}
-				></Dropdown>
-			</div>
-		{:else if subStep === 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={() => selectAnswer('education', selectedOption)}
-				></Dropdown>
-			</div>
-		{/if}
-	{:else}
-		{(step += 1)}
-	{/if}
-{:else if step === 4}
-	<div class="mx-auto mt-16 text-center">
-		<h1>{$t('surveys.complete')}</h1>
-		{#if finalScore !== null}
-			<p>{$t('surveys.score')} <strong>{finalScore} %</strong></p>
-		{/if}
-	</div>
-	{#if user == null}
-		<footer class="mt-auto text-center text-xs py-4">
-			{$t('register.consent.studyData.person')}: {$t('register.consent.studyData.personD')} - {$t(
-				'register.consent.studyData.email'
-			)}:
-			<a href="mailto:{$t('register.consent.studyData.emailD')}" class="link"
-				>{$t('register.consent.studyData.emailD')}</a
-			>
-		</footer>
-	{/if}
-{/if}
diff --git a/frontend/src/routes/tests/[id]/+page.ts b/frontend/src/routes/tests/[id]/+page.ts
deleted file mode 100644
index 62c35f258fa6bd1e92334e62c421ce1c47a341e9..0000000000000000000000000000000000000000
--- a/frontend/src/routes/tests/[id]/+page.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getSurveyAPI } from '$lib/api/survey';
-import Survey from '$lib/types/survey';
-import { error, type Load } from '@sveltejs/kit';
-
-export const ssr = false;
-
-export const load: Load = async ({ params, parent, fetch }) => {
-	const { user } = await parent();
-
-	if (!params.id) return error(400, 'Invalid survey ID');
-
-	const survey_id = parseInt(params.id);
-
-	if (isNaN(survey_id)) return error(400, 'Invalid survey ID');
-
-	const survey = Survey.parse(await getSurveyAPI(fetch, survey_id));
-
-	if (!survey) return error(404, 'Survey not found');
-
-	return {
-		survey,
-		user
-	};
-};
diff --git a/frontend/src/routes/tests/typing/+page.svelte b/frontend/src/routes/tests/typing/+page.svelte
deleted file mode 100644
index 17ec3c9122b225c5bc9fa3a475bc55fd3e1b2648..0000000000000000000000000000000000000000
--- a/frontend/src/routes/tests/typing/+page.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-<script lang="ts">
-	import Typingtest from '$lib/components/tests/typingtest.svelte';
-	import { t } from '$lib/services/i18n';
-
-	let finished = $state(false);
-
-	let { data } = $props();
-	let user = data.user;
-</script>
-
-{#if finished}
-	<p>{$t('surveys.complete')}</p>
-{:else}
-	<Typingtest {user} onFinish={() => (finished = true)} />
-{/if}
diff --git a/scripts/surveys/groups.csv b/scripts/surveys/groups.csv
index f45d894fabf9413d71e66e7d658c2ff76ff81529..3c700637005accfff7a40bb2241cca4fa8677a59 100644
--- a/scripts/surveys/groups.csv
+++ b/scripts/surveys/groups.csv
@@ -13,4 +13,3 @@ id,title,demo,options
 22,Vocabulary test T1,false,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230
 23,Vocabulary test T2,false,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245
 24,Vocabulary test T3,false,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260
-1100,basic-survey-questions,false,
\ No newline at end of file
diff --git a/scripts/surveys/items.csv b/scripts/surveys/items.csv
deleted file mode 100644
index 2f7c95ab915e65a17a662263997a90ed3cd9fd91..0000000000000000000000000000000000000000
--- a/scripts/surveys/items.csv
+++ /dev/null
@@ -1,244 +0,0 @@
-id,question,correct,option1,option2,option3,option4,option5,option6,option7,option8
-101,,1,,,,,,,,
-102,,1,,,,,,,,
-103,,1,,,,,,,,
-104,,1,,,,,,,,
-105,,1,,,,,,,,
-106,,1,,,,,,,,
-107,,1,,,,,,,,
-108,,1,,,,,,,,
-109,,1,,,,,,,,
-110,,1,,,,,,,,
-111,,1,,,,,,,,
-112,,1,,,,,,,,
-113,,1,,,,,,,,
-114,,1,,,,,,,,
-115,,1,,,,,,,,
-116,,1,,,,,,,,
-117,,1,,,,,,,,
-118,,1,,,,,,,,
-119,,1,,,,,,,,
-120,,1,,,,,,,,
-121,,1,,,,,,,,
-122,,1,,,,,,,,
-123,,1,,,,,,,,
-124,,1,,,,,,,,
-125,,1,,,,,,,,
-126,,1,,,,,,,,
-127,,1,,,,,,,,
-128,,1,,,,,,,,
-129,,1,,,,,,,,
-130,,1,,,,,,,,
-131,,1,,,,,,,,
-132,,1,,,,,,,,
-133,,1,,,,,,,,
-134,,1,,,,,,,,
-135,,1,,,,,,,,
-136,,1,,,,,,,,
-137,,1,,,,,,,,
-138,,1,,,,,,,,
-139,,1,,,,,,,,
-140,,1,,,,,,,,
-141,,1,,,,,,,,
-142,,1,,,,,,,,
-143,,1,,,,,,,,
-144,,1,,,,,,,,
-145,,1,,,,,,,,
-146,,1,,,,,,,,
-147,,1,,,,,,,,
-148,,1,,,,,,,,
-149,,1,,,,,,,,
-150,,1,,,,,,,,
-151,,1,,,,,,,,
-152,,1,,,,,,,,
-153,,1,,,,,,,,
-154,,1,,,,,,,,
-155,,1,,,,,,,,
-156,,1,,,,,,,,
-157,,1,,,,,,,,
-158,,1,,,,,,,,
-159,,1,,,,,,,,
-160,,1,,,,,,,,
-161,,1,,,,,,,,
-162,,1,,,,,,,,
-163,,1,,,,,,,,
-164,,1,,,,,,,,
-165,,1,,,,,,,,
-166,,1,,,,,,,,
-167,,1,,,,,,,,
-168,,1,,,,,,,,
-169,,1,,,,,,,,
-170,,1,,,,,,,,
-171,,1,,,,,,,,
-172,,1,,,,,,,,
-173,,1,,,,,,,,
-174,,1,,,,,,,,
-175,,1,,,,,,,,
-176,,1,,,,,,,,
-177,,1,,,,,,,,
-178,,1,,,,,,,,
-179,,1,,,,,,,,
-200,"My flowers are beauti<ful>.",-1
-201,"He has a successful car<eer> as a lawyer.",-1
-202,"Sudden noises at night sca<re> me a lot and keep me awake.",-1
-203,"Many people are inj<ured> in road accidents every year.",-1
-204,"Don't pay attention to this rude remark. Just ign<ore> it.",-1
-205,"There has been a recent tr<end> among prosperous families towards a smaller number of children.",-1
-206,"He is irresponsible. You cannot re<ly> on him for help.",-1
-207,"This work is not up to your usu<al> standard.",-1
-208,"You must have been very br<ave> to participate in such a dangerous operation.",-1
-209,"They sat down to eat even though they were not hu<ngry>.",-1
-210,"You must be awa<re> that very few jobs are available.",-1
-211,"The workmen cleaned up the me<ss> before they left.",-1
-212,"The drug was introduced after medical res<earch> indisputably proved its effectiveness.",-1
-213,"Governments often cut budgets in times of financial cri<sis>.",-1
-214,"In a lecture, most of the talking is done by the lecturer. In a seminar, students are expected to part<icipate> in the discussion.",-1
-215,"It's difficult to ass<ess> a person's true knowledge by one or two tests.",-1
-216,"There are a doz<en> eggs in the basket.",-1
-217,"The Far East is one of the most populated reg<ions> of the world.",-1
-218,"She spent her childhood in Europe and most of her ad<ult> life in Asia.",-1
-219,"La<ck> of rain led to a shortage of water in the city.",-1
-220,"His new book will be pub<lished> at the end of this month by a famous University Press.",-1
-221,"She didn't openly attack the plan, but her opposition was imp<licit> in her attitude.",-1
-222,"This sweater is too tight. It needs to be stret<ched>.",-1
-223,"In order to be accepted into the university, he had to impr<ove> his grades.",-1
-224,"He had been expe<lled> from school for stealing.",-1
-225,"Plants receive water from the soil though their ro<ots>.",-1
-226,"The children's games were funny at first, but finally got on the parents' ner<ves>.",-1
-227,"Before writing the final version, the student wrote several dra<fts>.",-1
-228,"I've had my eyes tested and the optician says my vi<sion> is good.",-1
-229,"In a free country, people can apply for any job. They should not be discriminated against on the basis of colour, age, or s<ex>.",-1
-230,"His decision to leave home was not well thought out. It was not based on rat<ional> considerations.",-1
-231,"They insp<ected> all products before sending them out to stores.",-1
-232,"The challenging job required a young, successful and dyn<amic> candidate.",-1
-233,"Even though I don't usually side with you, in this ins<tance> I must admit that you're right.",-1
-234,"If I were you I would con<tact> a good lawyer before taking action.",-1
-235,"The book covers a series of isolated epis<odes> from history.",-1
-236,"She is not a child, but a mat<ure> woman. She can make her own decisions.",-1
-237,"After finishing his degree, he entered upon a new ph<ase> in his career.",-1
-238,"The airport is far away. If you want to ens<ure> that you catch your plane, you have to leave early.",-1
-239,"The plaster on the wall was removed to exp<ose> the original bricks underneath.",-1
-240,"Farmers are introducing innova<tions> that increase the productivity per worker.",-1
-241,"A considerable amount of evidence was accum<ulated> during the investigation.",-1
-242,"Since he is unskilled, he earns low wa<ges>.",-1
-243,"Research ind<icates> that men find it easier to give up smoking than women.",-1
-244,"It is not easy to abs<orb> all this information in such a short time.",-1
-245,"People have proposed all kinds of hypot<heses> about what these things are.",-1
-246,"The lack of money depressed and frust<rated> him.",-1
-247,"The story tells us about a crime and sub<sequent> punishment.",-1
-248,"It's impossible to eva<luate> these results without knowing about the research methods that were used.",-1
-249,"I'm glad we had this opp<ortunity> to talk.",-1
-250,"The differences were so sl<ight> that they went unnoticed.",-1
-251,"To improve the country's economy, the government decided on economic ref<orms>.",-1
-252,"Laws are based upon the principle of jus<tice>.",-1
-253,"Anna intro<duced> her boyfriend to her mother last night.",-1
-254,"The dress you're wearing is lov<ely>.",-1
-255,"They had to cl<imb> a steep mountain to reach the cabin.",-1
-256,"The doctor ex<amined> the patient thoroughly.",-1
-257,"He takes cr<eam> and sugar in his coffee.",-1
-258,"Every year, the organisers li<mit> the number of participants to fifty.",-1
-259,"He usually reads the sport sec<tion> of the newspaper first.",-1
-260,"Teenagers often adm<ire> and worship pop singers.",-1
-1000,,1,,,,
-1001,,1,,,,
-1002,,1,,,,
-1003,,1,,,,
-1004,,1,,,,
-1005,,1,,,,
-1006,,1,,,,
-1007,,1,,,,
-1008,,1,,,,
-1009,,1,,,,
-1010,,1,,,,
-1011,,1,,,,
-1012,,1,,,,
-1013,,1,,,,
-1014,,1,,,,
-1015,,1,,,,
-1016,,1,,,,
-1017,,1,,,,
-1018,,1,,,,
-1019,,1,,,,
-1020,,1,,,,
-1021,,1,,,,
-1022,,1,,,,
-1023,,1,,,,
-1024,,1,,,,
-1025,,1,,,,
-1026,,1,,,,
-1027,,1,,,,
-1028,,1,,,,
-1029,,1,,,,
-1030,,1,,,,
-1031,,1,,,,
-1032,,1,,,,
-1033,,1,,,,
-
-1037,,1,,,,
-1038,,1,,,,
-1039,,1,,,,
-1040,,1,,,,
-1041,,1,,,,
-1042,,1,,,,
-1043,,1,,,,
-1044,,1,,,,
-1045,,1,,,,
-1046,,1,,,,
-1047,,1,,,,
-1048,,1,,,,
-1049,,1,,,,
-1050,,1,,,,
-1051,,1,,,,
-1052,,1,,,,
-1053,,1,,,,
-1054,,1,,,,
-1055,,1,,,,
-1056,,1,,,,
-1057,,1,,,,
-1058,,1,,,,
-1059,,1,,,,
-1060,,1,,,,
-1061,,1,,,,
-1062,,1,,,,
-1063,,1,,,,
-
-1065,,1,,,,
-1066,,1,,,,
-1067,,1,,,,
-1068,,1,,,,
-1069,,1,,,,
-
-1071,,1,,,,
-
-1073,,1,,,,
-
-1076,,1,,,,
-1077,,1,,,,
-1078,,1,,,,
-1079,,1,,,,
-1080,,1,,,,
-1081,,1,,,,
-
-1083,,1,,,,
-1084,,1,,,,
-1085,,1,,,,
-
-1087,,1,,,,
-1088,,1,,,,
-1089,,1,,,,
-
-1091,,1,,,,
-1092,,1,,,,
-1093,,1,,,,
-1094,,1,,,,
-1095,,1,,,,
-1096,,1,,,,
-1097,,1,,,,
-1098,,1,,,,
-1099,,1,,,,
-1100,,1,,,,
-1101,,1,,,,
-1102,,1,,,,
-1103,,1,,,,
-1104,,1,,,,
-1105,,1,,,,
\ No newline at end of file
diff --git a/scripts/surveys/questions_gapfill.csv b/scripts/surveys/questions_gapfill.csv
new file mode 100644
index 0000000000000000000000000000000000000000..6eaf6afa140e7e5d91464a327c4f99ba45ab9138
--- /dev/null
+++ b/scripts/surveys/questions_gapfill.csv
@@ -0,0 +1,62 @@
+id,question
+200,"My flowers are beauti<ful>."
+201,"He has a successful car<eer> as a lawyer."
+202,"Sudden noises at night sca<re> me a lot and keep me awake."
+203,"Many people are inj<ured> in road accidents every year."
+204,"Don't pay attention to this rude remark. Just ign<ore> it."
+205,"There has been a recent tr<end> among prosperous families towards a smaller number of children."
+206,"He is irresponsible. You cannot re<ly> on him for help."
+207,"This work is not up to your usu<al> standard."
+208,"You must have been very br<ave> to participate in such a dangerous operation."
+209,"They sat down to eat even though they were not hu<ngry>."
+210,"You must be awa<re> that very few jobs are available."
+211,"The workmen cleaned up the me<ss> before they left."
+212,"The drug was introduced after medical res<earch> indisputably proved its effectiveness."
+213,"Governments often cut budgets in times of financial cri<sis>."
+214,"In a lecture, most of the talking is done by the lecturer. In a seminar, students are expected to part<icipate> in the discussion."
+215,"It's difficult to ass<ess> a person's true knowledge by one or two tests."
+216,"There are a doz<en> eggs in the basket."
+217,"The Far East is one of the most populated reg<ions> of the world."
+218,"She spent her childhood in Europe and most of her ad<ult> life in Asia."
+219,"La<ck> of rain led to a shortage of water in the city."
+220,"His new book will be pub<lished> at the end of this month by a famous University Press."
+221,"She didn't openly attack the plan, but her opposition was imp<licit> in her attitude."
+222,"This sweater is too tight. It needs to be stret<ched>."
+223,"In order to be accepted into the university, he had to impr<ove> his grades."
+224,"He had been expe<lled> from school for stealing."
+225,"Plants receive water from the soil though their ro<ots>."
+226,"The children's games were funny at first, but finally got on the parents' ner<ves>."
+227,"Before writing the final version, the student wrote several dra<fts>."
+228,"I've had my eyes tested and the optician says my vi<sion> is good."
+229,"In a free country, people can apply for any job. They should not be discriminated against on the basis of colour, age, or s<ex>."
+230,"His decision to leave home was not well thought out. It was not based on rat<ional> considerations."
+231,"They insp<ected> all products before sending them out to stores."
+232,"The challenging job required a young, successful and dyn<amic> candidate."
+233,"Even though I don't usually side with you, in this ins<tance> I must admit that you're right."
+234,"If I were you I would con<tact> a good lawyer before taking action."
+235,"The book covers a series of isolated epis<odes> from history."
+236,"She is not a child, but a mat<ure> woman. She can make her own decisions."
+237,"After finishing his degree, he entered upon a new ph<ase> in his career."
+238,"The airport is far away. If you want to ens<ure> that you catch your plane, you have to leave early."
+239,"The plaster on the wall was removed to exp<ose> the original bricks underneath."
+240,"Farmers are introducing innova<tions> that increase the productivity per worker."
+241,"A considerable amount of evidence was accum<ulated> during the investigation."
+242,"Since he is unskilled, he earns low wa<ges>."
+243,"Research ind<icates> that men find it easier to give up smoking than women."
+244,"It is not easy to abs<orb> all this information in such a short time."
+245,"People have proposed all kinds of hypot<heses> about what these things are."
+246,"The lack of money depressed and frust<rated> him."
+247,"The story tells us about a crime and sub<sequent> punishment."
+248,"It's impossible to eva<luate> these results without knowing about the research methods that were used."
+249,"I'm glad we had this opp<ortunity> to talk."
+250,"The differences were so sl<ight> that they went unnoticed."
+251,"To improve the country's economy, the government decided on economic ref<orms>."
+252,"Laws are based upon the principle of jus<tice>."
+253,"Anna intro<duced> her boyfriend to her mother last night."
+254,"The dress you're wearing is lov<ely>."
+255,"They had to cl<imb> a steep mountain to reach the cabin."
+256,"The doctor ex<amined> the patient thoroughly."
+257,"He takes cr<eam> and sugar in his coffee."
+258,"Every year, the organisers li<mit> the number of participants to fifty."
+259,"He usually reads the sport sec<tion> of the newspaper first."
+260,"Teenagers often adm<ire> and worship pop singers."
diff --git a/scripts/surveys/questions_qcm.csv b/scripts/surveys/questions_qcm.csv
new file mode 100644
index 0000000000000000000000000000000000000000..b2a52e64f43771a226977fc386b54d189a85f9aa
--- /dev/null
+++ b/scripts/surveys/questions_qcm.csv
@@ -0,0 +1,175 @@
+id,question,correct,option1,option2,option3,option4,option5,option6,option7,option8
+101,,1,,,,,,,,
+102,,1,,,,,,,,
+103,,1,,,,,,,,
+104,,1,,,,,,,,
+105,,1,,,,,,,,
+106,,1,,,,,,,,
+107,,1,,,,,,,,
+108,,1,,,,,,,,
+109,,1,,,,,,,,
+110,,1,,,,,,,,
+111,,1,,,,,,,,
+112,,1,,,,,,,,
+113,,1,,,,,,,,
+114,,1,,,,,,,,
+115,,1,,,,,,,,
+116,,1,,,,,,,,
+117,,1,,,,,,,,
+118,,1,,,,,,,,
+119,,1,,,,,,,,
+120,,1,,,,,,,,
+121,,1,,,,,,,,
+122,,1,,,,,,,,
+123,,1,,,,,,,,
+124,,1,,,,,,,,
+125,,1,,,,,,,,
+126,,1,,,,,,,,
+127,,1,,,,,,,,
+128,,1,,,,,,,,
+129,,1,,,,,,,,
+130,,1,,,,,,,,
+131,,1,,,,,,,,
+132,,1,,,,,,,,
+133,,1,,,,,,,,
+134,,1,,,,,,,,
+135,,1,,,,,,,,
+136,,1,,,,,,,,
+137,,1,,,,,,,,
+138,,1,,,,,,,,
+139,,1,,,,,,,,
+140,,1,,,,,,,,
+141,,1,,,,,,,,
+142,,1,,,,,,,,
+143,,1,,,,,,,,
+144,,1,,,,,,,,
+145,,1,,,,,,,,
+146,,1,,,,,,,,
+147,,1,,,,,,,,
+148,,1,,,,,,,,
+149,,1,,,,,,,,
+150,,1,,,,,,,,
+151,,1,,,,,,,,
+152,,1,,,,,,,,
+153,,1,,,,,,,,
+154,,1,,,,,,,,
+155,,1,,,,,,,,
+156,,1,,,,,,,,
+157,,1,,,,,,,,
+158,,1,,,,,,,,
+159,,1,,,,,,,,
+160,,1,,,,,,,,
+161,,1,,,,,,,,
+162,,1,,,,,,,,
+163,,1,,,,,,,,
+164,,1,,,,,,,,
+165,,1,,,,,,,,
+166,,1,,,,,,,,
+167,,1,,,,,,,,
+168,,1,,,,,,,,
+169,,1,,,,,,,,
+170,,1,,,,,,,,
+171,,1,,,,,,,,
+172,,1,,,,,,,,
+173,,1,,,,,,,,
+174,,1,,,,,,,,
+175,,1,,,,,,,,
+176,,1,,,,,,,,
+177,,1,,,,,,,,
+178,,1,,,,,,,,
+179,,1,,,,,,,,
+1000,,1,,,,,,,,
+1001,,1,,,,,,,,
+1002,,1,,,,,,,,
+1003,,1,,,,,,,,
+1004,,1,,,,,,,,
+1005,,1,,,,,,,,
+1006,,1,,,,,,,,
+1007,,1,,,,,,,,
+1008,,1,,,,,,,,
+1009,,1,,,,,,,,
+1010,,1,,,,,,,,
+1011,,1,,,,,,,,
+1012,,1,,,,,,,,
+1013,,1,,,,,,,,
+1014,,1,,,,,,,,
+1015,,1,,,,,,,,
+1016,,1,,,,,,,,
+1017,,1,,,,,,,,
+1018,,1,,,,,,,,
+1019,,1,,,,,,,,
+1020,,1,,,,,,,,
+1021,,1,,,,,,,,
+1022,,1,,,,,,,,
+1023,,1,,,,,,,,
+1024,,1,,,,,,,,
+1025,,1,,,,,,,,
+1026,,1,,,,,,,,
+1027,,1,,,,,,,,
+1028,,1,,,,,,,,
+1029,,1,,,,,,,,
+1030,,1,,,,,,,,
+1031,,1,,,,,,,,
+1032,,1,,,,,,,,
+1033,,1,,,,,,,,
+1037,,1,,,,,,,,
+1038,,1,,,,,,,,
+1039,,1,,,,,,,,
+1040,,1,,,,,,,,
+1041,,1,,,,,,,,
+1042,,1,,,,,,,,
+1043,,1,,,,,,,,
+1044,,1,,,,,,,,
+1045,,1,,,,,,,,
+1046,,1,,,,,,,,
+1047,,1,,,,,,,,
+1048,,1,,,,,,,,
+1049,,1,,,,,,,,
+1050,,1,,,,,,,,
+1051,,1,,,,,,,,
+1052,,1,,,,,,,,
+1053,,1,,,,,,,,
+1054,,1,,,,,,,,
+1055,,1,,,,,,,,
+1056,,1,,,,,,,,
+1057,,1,,,,,,,,
+1058,,1,,,,,,,,
+1059,,1,,,,,,,,
+1060,,1,,,,,,,,
+1061,,1,,,,,,,,
+1062,,1,,,,,,,,
+1063,,1,,,,,,,,
+1065,,1,,,,,,,,
+1066,,1,,,,,,,,
+1067,,1,,,,,,,,
+1068,,1,,,,,,,,
+1069,,1,,,,,,,,
+1071,,1,,,,,,,,
+1073,,1,,,,,,,,
+1076,,1,,,,,,,,
+1077,,1,,,,,,,,
+1078,,1,,,,,,,,
+1079,,1,,,,,,,,
+1080,,1,,,,,,,,
+1081,,1,,,,,,,,
+1083,,1,,,,,,,,
+1084,,1,,,,,,,,
+1085,,1,,,,,,,,
+1087,,1,,,,,,,,
+1088,,1,,,,,,,,
+1089,,1,,,,,,,,
+1091,,1,,,,,,,,
+1092,,1,,,,,,,,
+1093,,1,,,,,,,,
+1094,,1,,,,,,,,
+1095,,1,,,,,,,,
+1096,,1,,,,,,,,
+1097,,1,,,,,,,,
+1098,,1,,,,,,,,
+1099,,1,,,,,,,,
+1100,,1,,,,,,,,
+1101,,1,,,,,,,,
+1102,,1,,,,,,,,
+1103,,1,,,,,,,,
+1104,,1,,,,,,,,
+1105,,1,,,,,,,,
diff --git a/scripts/surveys/survey_maker.py b/scripts/surveys/survey_maker.py
index 05c7cb4fc8c264665c877cd90befe6526f372720..8d1b3d9c29120fca192d8a1946c43d0084760f67 100644
--- a/scripts/surveys/survey_maker.py
+++ b/scripts/surveys/survey_maker.py
@@ -8,23 +8,20 @@ API_PATH = "/tmp-api/v1"
 LOCAL_ITEMS_FOLDER = "../../frontend/static/surveys/items"
 REMOTE_ITEMS_FOLDER = "/surveys/items"
 
-# PARSE ITEMS
+# PARSE QCM QUESTIONS
 
-df_items = pd.read_csv("items.csv", dtype=str)
+df_questions_qcm = pd.read_csv("questions_qcm.csv", dtype=str)
 
-items = []
-for i, row in df_items.iterrows():
+questions_qcm = []
+for i, row in df_questions_qcm.iterrows():
     row = row.dropna()
     id_ = int(row["id"])
 
-    o = {"id": id_, "question": None, "correct": None}
-    items.append(o)
+    o = {"id": id_, "question": None, "question_qcm": {"correct": None}}
+    questions_qcm.append(o)
 
     if "question" in row:
-        if re.search(r"<.*?>", str(row["question"])):
-            o["question"] = f'gap:{row["question"]}'
-        else:
-            o["question"] = f'text:{row["question"]}'
+        o["question"] = f'text:{row["question"]}'
     elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/q.mp3"):
         o["question"] = f"audio:{REMOTE_ITEMS_FOLDER}/{id_}/q.mp3"
     elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/q.jpeg"):
@@ -33,7 +30,7 @@ for i, row in df_items.iterrows():
         print(f"Failed to find a question for item {id_}")
 
     if "correct" in row:
-        o["correct"] = int(row["correct"])
+        o["question_qcm"]["correct"] = int(row["correct"])
     else:
         print(f"Failed to find corect for item {id_}")
 
@@ -41,22 +38,44 @@ for i, row in df_items.iterrows():
         with open(f"{LOCAL_ITEMS_FOLDER}/{id_}/1_dropdown.txt", "r") as file:
             options = file.read().split(",")
         options = [option.strip() for option in options]
-        o[f"option1"] = f"dropdown:{', '.join(options)}"
+        o["question_qcm"][f"option1"] = f"dropdown:{', '.join(options)}"
     elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/1_radio.txt"):
         with open(f"{LOCAL_ITEMS_FOLDER}/{id_}/1_radio.txt", "r") as file:
             options = file.read().split(",")
         options = [option.strip() for option in options]
-        o[f"option1"] = f"radio:{', '.join(options)}"
+        o["question_qcm"][f"option1"] = f"radio:{', '.join(options)}"
     else:
         for j in range(1, 9):
             op = f"option{j}"
             if op in row:
-                o[op] = "text:" + row[op]
+                o["question_qcm"][op] = "text:" + row[op]
             elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/{j}.mp3"):
-                o[op] = f"audio:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.mp3"
+                o["question_qcm"][op] = f"audio:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.mp3"
             elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/{j}.jpeg"):
-                o[op] = f"image:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.jpeg"
-    # print(o)
+                o["question_qcm"][op] = f"image:{REMOTE_ITEMS_FOLDER}/{id_}/{j}.jpeg"
+
+# PARSE GAPFILL QUESTIONS
+
+df_questions_gapfill = pd.read_csv("questions_gapfill.csv", dtype=str)
+
+questions_gapfill = []
+for i, row in df_questions_gapfill.iterrows():
+    row = row.dropna()
+    id_ = int(row["id"])
+
+    o = {"id": id_, "question": None}
+    questions_gapfill.append(o)
+
+    if "question" in row:
+        o["question"] = f'text:{row["question"]}'
+    elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/q.mp3"):
+        o["question"] = f"audio:{REMOTE_ITEMS_FOLDER}/{id_}/q.mp3"
+    elif os.path.isfile(f"{LOCAL_ITEMS_FOLDER}/{id_}/q.jpeg"):
+        o["question"] = f"image:{REMOTE_ITEMS_FOLDER}/{id_}/q.jpeg"
+    else:
+        print(f"Failed to find a question for item {id_}")
+
+
 # PARSE GROUPS
 
 groups = []
@@ -71,10 +90,10 @@ with open("groups.csv") as file:
         its = [int(x) for x in its if x]
         groups.append({"id": id_, "title": title, "demo": demo_, "items_id": its})
 
-# PARSE SURVEYS
+# PARSE TASK TESTS
 
-surveys = []
-with open("surveys.csv") as file:
+tests_task = []
+with open("tests_task.csv") as file:
     file.readline()
     for line in file.read().split("\n"):
         if not line:
@@ -82,7 +101,28 @@ with open("surveys.csv") as file:
         id_, title, *gps = line.split(",")
         id_ = int(id_)
         gps = [int(x) for x in gps if x]
-        surveys.append({"id": id_, "title": title, "groups_id": gps})
+        tests_task.append(
+            {"id": id_, "title": title, "test_task": {"groups": []}, "groups_id": gps}
+        )
+
+# PARSE TYPING TESTS
+
+df_typing_test = pd.read_csv("tests_typing.csv", dtype=str)
+
+tests_typing = []
+for i, row in df_typing_test.iterrows():
+    tests_typing.append(
+        {
+            "id": int(row["id"]),
+            "title": row["title"],
+            "test_typing": {
+                "explanations": row["explanations"],
+                "text": row["text"],
+                "repeat": int(str(row["repeat"])),
+                "duration": int(str(row["duration"])),
+            },
+        }
+    )
 
 # SESSION DATA
 
@@ -99,22 +139,47 @@ assert (
     response_code == 200
 ), f"Probably wrong username or password. Status code: {response_code}"
 
-# CREATE ITEMS
+# CREATE QUESTIONS QCM
 
-n_items = 0
+n_questions_qcm = 0
 
-for item in items:
+for q in questions_qcm:
     assert session.delete(
-        f'{API_URL}{API_PATH}/surveys/items/{item["id"]}'
-    ).status_code in [404, 204], f'Failed to delete item {item["id"]}'
-    r = session.post(f"{API_URL}{API_PATH}/surveys/items", json=item)
+        f'{API_URL}{API_PATH}/tests/questions/{q["id"]}'
+    ).status_code in [404, 204], f'Failed to delete item {q["id"]}'
+    r = session.post(f"{API_URL}{API_PATH}/tests/questions", json=q)
     if r.status_code not in [201]:
-        print(f'Failed to create item {item["id"]}: {r.text}')
+        print(f'Failed to create item {q["id"]}: {r.text}')
         continue
-    else:
-        n_items += 1
+
+    if r.text != str(q["id"]):
+        print(f'Item {q["id"]} was created with id {r.text}')
+
+    n_questions_qcm += 1
+else:
+    print(f"Successfully created {n_questions_qcm}/{len(questions_qcm)} qcm questions")
+
+# CREATE QUESTIONS GAPFILL
+
+n_questions_gapfill = 0
+
+for q in questions_gapfill:
+    assert session.delete(
+        f'{API_URL}{API_PATH}/tests/questions/{q["id"]}'
+    ).status_code in [404, 204], f'Failed to delete item {q["id"]}'
+    r = session.post(f"{API_URL}{API_PATH}/tests/questions", json=q)
+    if r.status_code not in [201]:
+        print(f'Failed to create item {q["id"]}: {r.text}')
+        continue
+
+    if r.text != str(q["id"]):
+        print(f'Item {q["id"]} was created with id {r.text}')
+
+    n_questions_gapfill += 1
 else:
-    print(f"Successfully created {n_items}/{len(items)} items")
+    print(
+        f"Successfully created {n_questions_gapfill}/{len(questions_gapfill)} gapfill questions"
+    )
 
 # CREATE GROUPS
 
@@ -124,25 +189,27 @@ for group in groups:
     group = group.copy()
     its = group.pop("items_id")
     assert session.delete(
-        f'{API_URL}{API_PATH}/surveys/groups/{group["id"]}'
+        f'{API_URL}{API_PATH}/tests/groups/{group["id"]}'
     ).status_code in [404, 204], f'Failed to delete group {group["id"]}'
-    r = session.post(f"{API_URL}{API_PATH}/surveys/groups", json=group)
+    r = session.post(f"{API_URL}{API_PATH}/tests/groups", json=group)
     if r.status_code not in [201]:
         print(f'Failed to create group {group["id"]}: {r.text}')
         continue
-    else:
-        n_groups += 1
+
+    if r.text != str(group["id"]):
+        print(f'Group {group["id"]} was created with id {r.text}')
+
+    n_groups += 1
 
     for it in its:
         assert session.delete(
-            f'{API_URL}{API_PATH}/surveys/groups/{group["id"]}/items/{it}'
+            f'{API_URL}{API_PATH}/tests/groups/{group["id"]}/questions/{it}'
         ).status_code in [
             404,
             204,
-        ], f'Failed to delete item {it} from group {group["id"]}'
+        ], f'Failed to delete question {it} from group {group["id"]}'
         r = session.post(
-            f'{API_URL}{API_PATH}/surveys/groups/{group["id"]}/items',
-            json={"question_id": it},
+            f'{API_URL}{API_PATH}/tests/groups/{group["id"]}/questions/{it}'
         )
         if r.status_code not in [201]:
             print(f'Failed to add item {it} to group {group["id"]}: {r.text}')
@@ -151,39 +218,60 @@ for group in groups:
 else:
     print(f"Successfully created {n_groups}/{len(groups)} groups")
 
-# CREATE SURVEYS
+# CREATE TASK TESTS
 
-n_surveys = 0
+n_task_tests = 0
 
-for survey in surveys:
-    survey = survey.copy()
-    gps = survey.pop("groups_id")
-    assert session.delete(
-        f'{API_URL}{API_PATH}/surveys/{survey["id"]}'
-    ).status_code in [
+for t in tests_task:
+    t = t.copy()
+    gps = t.pop("groups_id")
+    assert session.delete(f'{API_URL}{API_PATH}/tests/{t["id"]}').status_code in [
         404,
         204,
-    ], f'Failed to delete survey {survey["id"]}'
-    r = session.post(f"{API_URL}{API_PATH}/surveys", json=survey)
+    ], f'Failed to delete test {t["id"]}'
+    r = session.post(f"{API_URL}{API_PATH}/tests", json=t)
     if r.status_code not in [201]:
-        print(f'Failed to create suvey {survey["id"]}: {r.text}')
+        print(f'Failed to create suvey {t["id"]}: {r.text}')
         continue
-    else:
-        n_surveys += 1
+
+    if r.text != str(t["id"]):
+        print(f'Survey {t["id"]} was created with id {r.text}')
+
+    n_task_tests += 1
 
     for gp in gps:
         assert session.delete(
-            f'{API_URL}{API_PATH}/surveys/{survey["id"]}/groups/{gp}'
+            f'{API_URL}{API_PATH}/tests/{t["id"]}/groups/{gp}'
         ).status_code in [
             404,
             204,
-        ], f'Failed to delete gp {gp} from survey {survey["id"]}'
-        r = session.post(
-            f'{API_URL}{API_PATH}/surveys/{survey["id"]}/groups', json={"group_id": gp}
-        )
+        ], f'Failed to delete gp {gp} from test {t["id"]}'
+        r = session.post(f'{API_URL}{API_PATH}/tests/{t["id"]}/groups/{gp}')
         if r.status_code not in [201]:
-            print(f'Failed to add group {gp} to survey {survey["id"]}: {r.text}')
+            print(f'Failed to add group {gp} to test {t["id"]}: {r.text}')
             break
+else:
+    print(f"Successfully created {n_task_tests}/{len(tests_task)} task tests")
+
+# CREATE TYPING TESTS
+
+n_typing_tests = 0
+
+for t in tests_typing:
+    assert session.delete(f'{API_URL}{API_PATH}/tests/{t["id"]}').status_code in [
+        404,
+        204,
+    ], f'Failed to delete test {t["id"]}'
+
+    r = session.post(f"{API_URL}{API_PATH}/tests", json=t)
+    if r.status_code not in [201]:
+        print(f'Failed to create typing test {t["id"]}: {r.text}')
+        continue
+
+    if r.text != str(t["id"]):
+        print(f'Typing test {t["id"]} was created with id {r.text}')
+
+    n_typing_tests += 1
 
 else:
-    print(f"Successfully created {n_surveys}/{len(surveys)} surveys")
+    print(f"Successfully created {n_typing_tests}/{len(tests_typing)} typing tests")
diff --git a/scripts/surveys/surveys.csv b/scripts/surveys/surveys.csv
deleted file mode 100644
index 981ba8288a32e5e32b0978a11e104b4cf2941cde..0000000000000000000000000000000000000000
--- a/scripts/surveys/surveys.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-id,title,groups
-1,Auditory Picture Vocabulary Test - English,3,1,1100
-2,APVT English Demo,3,2,1100
-3,APVT Français,10,11,12,13,14,15,1100
-4,Vocabulary test T1,20,21,22,1100
-5,Vocabulary test T2,21,23
-6,Vocabulary test T3,21,24
\ No newline at end of file
diff --git a/scripts/surveys/tests_task.csv b/scripts/surveys/tests_task.csv
new file mode 100644
index 0000000000000000000000000000000000000000..0e428318ea67ac151df86b94ace5d99a8730cef8
--- /dev/null
+++ b/scripts/surveys/tests_task.csv
@@ -0,0 +1,7 @@
+id,title,groups
+1,Auditory Picture Vocabulary Test - English,3,1
+2,APVT English Demo,3,2
+3,APVT Français,10,11,12,13,14,15
+4,Vocabulary test T1,20,21,22
+5,Vocabulary test T2,21,23
+6,Vocabulary test T3,21,24
diff --git a/scripts/surveys/tests_typing.csv b/scripts/surveys/tests_typing.csv
new file mode 100644
index 0000000000000000000000000000000000000000..dbd53f98b44270dbc351e2ab94aa77bee6c07cbd
--- /dev/null
+++ b/scripts/surveys/tests_typing.csv
@@ -0,0 +1,5 @@
+id,title,explanations,text,repeat,duration
+7,"Repeat letters","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 commencer. Une vois que vous aurez terminé, appuyez sur le bouton suivant pour passer à l'exercice suivant.","dk",0,15
+8,"Repeat a sentence","Repetez la phrase suivante autant de fois que possible en 30 secondes.","Le chat est sur le toit.",0,30
+9,"Repeat 7","Repetez 7 fois la phrase suivante le plus rapidement possible.","Six animaux mangent",7,0
+