From 66f5aeaf519d394e373e3b60962659000cd616a3 Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Thu, 14 Nov 2024 10:37:33 +0100 Subject: [PATCH] Allow admins to view and edit users - backend - add /admin/users/ endpoint to get a list of users - refactor admin endpoints into separate files for languages, milestones, users, questions - refactor tests to also create a temporary in-memory users database for each test - add pytest-asyncio to allow use of async fixtures - refactor tests to be fully independent of each other - new app & new temp dirs for each test - makes test suite slower to run but easier to maintain - remove DATA_FILES_PATH as we already have STATIC_FILES_PATH and PRIVATE_FILES_PATH - admin-frontend - add users tab with table of users whose permissions can be edited - resolves #164 --- docker-compose.yml | 1 - frontend/src/lib/client/services.gen.ts | 12 +- frontend/src/lib/client/types.gen.ts | 4 + .../src/lib/components/Admin/Users.svelte | 99 ++++ frontend/src/lib/components/AdminPage.svelte | 9 + frontend/src/locales/de.json | 1 + mondey_backend/openapi.json | 2 +- mondey_backend/pyproject.toml | 3 +- .../src/mondey_backend/dependencies.py | 4 + .../src/mondey_backend/routers/admin.py | 268 +-------- .../routers/admin_routers/__init__.py | 0 .../routers/admin_routers/languages.py | 53 ++ .../routers/admin_routers/milestones.py | 142 +++++ .../routers/admin_routers/questions.py | 107 ++++ .../routers/admin_routers/users.py | 19 + .../src/mondey_backend/routers/users.py | 4 +- mondey_backend/src/mondey_backend/settings.py | 1 - mondey_backend/tests/conftest.py | 126 +++-- .../admin_routers/test_admin_languages.py | 64 +++ .../admin_routers/test_admin_milestones.py | 220 ++++++++ .../admin_routers/test_admin_questions.py | 249 ++++++++ .../routers/admin_routers/test_admin_users.py | 20 + mondey_backend/tests/routers/test_admin.py | 531 ------------------ mondey_backend/tests/routers/test_users.py | 17 +- 24 files changed, 1100 insertions(+), 856 deletions(-) create mode 100644 frontend/src/lib/components/Admin/Users.svelte create mode 100644 mondey_backend/src/mondey_backend/routers/admin_routers/__init__.py create mode 100644 mondey_backend/src/mondey_backend/routers/admin_routers/languages.py create mode 100644 mondey_backend/src/mondey_backend/routers/admin_routers/milestones.py create mode 100644 mondey_backend/src/mondey_backend/routers/admin_routers/questions.py create mode 100644 mondey_backend/src/mondey_backend/routers/admin_routers/users.py create mode 100644 mondey_backend/tests/routers/admin_routers/test_admin_languages.py create mode 100644 mondey_backend/tests/routers/admin_routers/test_admin_milestones.py create mode 100644 mondey_backend/tests/routers/admin_routers/test_admin_questions.py create mode 100644 mondey_backend/tests/routers/admin_routers/test_admin_users.py delete mode 100644 mondey_backend/tests/routers/test_admin.py diff --git a/docker-compose.yml b/docker-compose.yml index 1d6e257a..893a81c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,6 @@ services: - ${PRIVATE_FILES_PATH:-./private}:/app/private environment: - SECRET=${SECRET:-} - - DATA_FILES_PATH=/app/data - STATIC_FILES_PATH=/app/static - PRIVATE_FILES_PATH=/app/private - DATABASE_PATH=/app/db diff --git a/frontend/src/lib/client/services.gen.ts b/frontend/src/lib/client/services.gen.ts index d62c9c5f..19ab4fe3 100644 --- a/frontend/src/lib/client/services.gen.ts +++ b/frontend/src/lib/client/services.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type Options, formDataBodySerializer, urlSearchParamsBodySerializer } from '@hey-api/client-fetch'; -import type { GetLanguagesError, GetLanguagesResponse, GetMilestonesError, GetMilestonesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, GetUserQuestionsError, GetUserQuestionsResponse, GetChildQuestionsError, GetChildQuestionsResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, UsersCurrentUserError, UsersCurrentUserResponse, UsersPatchCurrentUserData, UsersPatchCurrentUserError, UsersPatchCurrentUserResponse, UsersUserData, UsersUserError, UsersUserResponse, UsersPatchUserData, UsersPatchUserError, UsersPatchUserResponse, UsersDeleteUserData, UsersDeleteUserError, UsersDeleteUserResponse, GetChildrenError, GetChildrenResponse, UpdateChildData, UpdateChildError, UpdateChildResponse, CreateChildData, CreateChildError, CreateChildResponse, GetChildData, GetChildError, GetChildResponse, DeleteChildData, DeleteChildError, DeleteChildResponse, GetChildImageData, GetChildImageError, GetChildImageResponse, UploadChildImageData, UploadChildImageError, UploadChildImageResponse, DeleteChildImageData, DeleteChildImageError, DeleteChildImageResponse, GetCurrentMilestoneAnswerSessionData, GetCurrentMilestoneAnswerSessionError, GetCurrentMilestoneAnswerSessionResponse, UpdateMilestoneAnswerData, UpdateMilestoneAnswerError, UpdateMilestoneAnswerResponse, GetCurrentUserAnswersError, GetCurrentUserAnswersResponse, UpdateCurrentUserAnswersData, UpdateCurrentUserAnswersError, UpdateCurrentUserAnswersResponse, GetCurrentChildAnswersData, GetCurrentChildAnswersError, GetCurrentChildAnswersResponse, UpdateCurrentChildAnswersData, UpdateCurrentChildAnswersError, UpdateCurrentChildAnswersResponse, AuthCookieLoginData, AuthCookieLoginError, AuthCookieLoginResponse, AuthCookieLogoutError, AuthCookieLogoutResponse, RegisterRegisterData, RegisterRegisterError, RegisterRegisterResponse, ResetForgotPasswordData, ResetForgotPasswordError, ResetForgotPasswordResponse, ResetResetPasswordData, ResetResetPasswordError, ResetResetPasswordResponse, VerifyRequestTokenData, VerifyRequestTokenError, VerifyRequestTokenResponse, VerifyVerifyData, VerifyVerifyError, VerifyVerifyResponse, AuthError, AuthResponse } from './types.gen'; +import type { GetLanguagesError, GetLanguagesResponse, GetMilestonesError, GetMilestonesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, GetUserQuestionsError, GetUserQuestionsResponse, GetChildQuestionsError, GetChildQuestionsResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, GetUsersError, GetUsersResponse, UsersCurrentUserError, UsersCurrentUserResponse, UsersPatchCurrentUserData, UsersPatchCurrentUserError, UsersPatchCurrentUserResponse, UsersUserData, UsersUserError, UsersUserResponse, UsersPatchUserData, UsersPatchUserError, UsersPatchUserResponse, UsersDeleteUserData, UsersDeleteUserError, UsersDeleteUserResponse, GetChildrenError, GetChildrenResponse, UpdateChildData, UpdateChildError, UpdateChildResponse, CreateChildData, CreateChildError, CreateChildResponse, GetChildData, GetChildError, GetChildResponse, DeleteChildData, DeleteChildError, DeleteChildResponse, GetChildImageData, GetChildImageError, GetChildImageResponse, UploadChildImageData, UploadChildImageError, UploadChildImageResponse, DeleteChildImageData, DeleteChildImageError, DeleteChildImageResponse, GetCurrentMilestoneAnswerSessionData, GetCurrentMilestoneAnswerSessionError, GetCurrentMilestoneAnswerSessionResponse, UpdateMilestoneAnswerData, UpdateMilestoneAnswerError, UpdateMilestoneAnswerResponse, GetCurrentUserAnswersError, GetCurrentUserAnswersResponse, UpdateCurrentUserAnswersData, UpdateCurrentUserAnswersError, UpdateCurrentUserAnswersResponse, GetCurrentChildAnswersData, GetCurrentChildAnswersError, GetCurrentChildAnswersResponse, UpdateCurrentChildAnswersData, UpdateCurrentChildAnswersError, UpdateCurrentChildAnswersResponse, AuthCookieLoginData, AuthCookieLoginError, AuthCookieLoginResponse, AuthCookieLogoutError, AuthCookieLogoutResponse, RegisterRegisterData, RegisterRegisterError, RegisterRegisterResponse, ResetForgotPasswordData, ResetForgotPasswordError, ResetForgotPasswordResponse, ResetResetPasswordData, ResetResetPasswordError, ResetResetPasswordResponse, VerifyRequestTokenData, VerifyRequestTokenError, VerifyRequestTokenResponse, VerifyVerifyData, VerifyVerifyError, VerifyVerifyResponse, AuthError, AuthResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -295,6 +295,16 @@ export const deleteChildQuestion = (option }); }; +/** + * Get Users + */ +export const getUsers = (options?: Options) => { + return (options?.client ?? client).get({ + ...options, + url: '/admin/users/' + }); +}; + /** * Users:Current User */ diff --git a/frontend/src/lib/client/types.gen.ts b/frontend/src/lib/client/types.gen.ts index de90b962..0cd1e1ab 100644 --- a/frontend/src/lib/client/types.gen.ts +++ b/frontend/src/lib/client/types.gen.ts @@ -496,6 +496,10 @@ export type DeleteChildQuestionResponse = (unknown); export type DeleteChildQuestionError = (HTTPValidationError); +export type GetUsersResponse = (Array); + +export type GetUsersError = unknown; + export type UsersCurrentUserResponse = (UserRead); export type UsersCurrentUserError = (unknown); diff --git a/frontend/src/lib/components/Admin/Users.svelte b/frontend/src/lib/components/Admin/Users.svelte new file mode 100644 index 00000000..9cb6522f --- /dev/null +++ b/frontend/src/lib/components/Admin/Users.svelte @@ -0,0 +1,99 @@ + + + + + +

+ {$_("admin.users")} +

+ + + Email + Active + Verified + Researcher + Admin + Actions + + + {#each users as user (user.id)} + + + {user.email} + + + {saveDisabled[user.id]=false}} /> + + + {saveDisabled[user.id]=false}} /> + + + {saveDisabled[user.id]=false}} /> + + + {saveDisabled[user.id]=false}} /> + + + {updateUser(user)}} /> + + + {/each} + +
+
diff --git a/frontend/src/lib/components/AdminPage.svelte b/frontend/src/lib/components/AdminPage.svelte index 8f8b056c..acbfd82c 100644 --- a/frontend/src/lib/components/AdminPage.svelte +++ b/frontend/src/lib/components/AdminPage.svelte @@ -7,12 +7,14 @@ import MilestoneExpectedAges from "$lib/components/Admin/MilestoneExpectedAges.s import MilestoneGroups from "$lib/components/Admin/MilestoneGroups.svelte"; import Questions from "$lib/components/Admin/Questions.svelte"; import Translations from "$lib/components/Admin/Translations.svelte"; +import Users from "$lib/components/Admin/Users.svelte"; import { TabItem, Tabs } from "flowbite-svelte"; import { BadgeCheckOutline, ClipboardListOutline, LanguageOutline, ScaleBalancedOutline, + UsersOutline, } from "flowbite-svelte-icons"; import { onMount } from "svelte"; import { _ } from "svelte-i18n"; @@ -38,6 +40,13 @@ onMount(async () => { + +
+ + {$_("admin.users")} +
+ +
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index fc4cd73f..8e06a2fe 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -22,6 +22,7 @@ "milestones": "Meilensteine", "milestone-groups": "Meilensteingruppen", "translations": "Übersetzungen", + "users": "Benutzer", "user-questions": "Fragen über Beobachter", "child-questions": "Fragen über Kind", "question": "Frage", diff --git a/mondey_backend/openapi.json b/mondey_backend/openapi.json index 78f28176..c9a848d2 100644 --- a/mondey_backend/openapi.json +++ b/mondey_backend/openapi.json @@ -1 +1 @@ -{"openapi": "3.1.0", "info": {"title": "MONDEY API", "version": "0.1.0"}, "paths": {"/languages/": {"get": {"tags": ["milestones"], "summary": "Get Languages", "operationId": "get_languages", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"type": "string"}, "type": "array", "title": "Response Get Languages Languages Get"}}}}}}}, "/milestones/": {"get": {"tags": ["milestones"], "summary": "Get Milestones", "operationId": "get_milestones", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/MilestonePublic"}, "type": "array", "title": "Response Get Milestones Milestones Get"}}}}}}}, "/milestones/{milestone_id}": {"get": {"tags": ["milestones"], "summary": "Get Milestone", "operationId": "get_milestone", "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestonePublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/milestone-groups/{child_id}": {"get": {"tags": ["milestones"], "summary": "Get Milestone Groups", "operationId": "get_milestone_groups", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/MilestoneGroupPublic"}, "title": "Response Get Milestone Groups Milestone Groups Child Id Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/user-questions/": {"get": {"tags": ["questions"], "summary": "Get User Questions", "operationId": "get_user_questions", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserQuestionPublic"}, "type": "array", "title": "Response Get User Questions User Questions Get"}}}}}}}, "/child-questions/": {"get": {"tags": ["questions"], "summary": "Get Child Questions", "operationId": "get_child_questions", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildQuestionPublic"}, "type": "array", "title": "Response Get Child Questions Child Questions Get"}}}}}}}, "/admin/languages/": {"post": {"tags": ["admin"], "summary": "Create Language", "operationId": "create_language", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Language"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Language"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/languages/{language_id}": {"delete": {"tags": ["admin"], "summary": "Delete Language", "operationId": "delete_language", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "language_id", "in": "path", "required": true, "schema": {"type": "string", "title": "Language Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/i18n/{language_id}": {"put": {"tags": ["admin"], "summary": "Update I18N", "operationId": "update_i18n", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "language_id", "in": "path", "required": true, "schema": {"type": "string", "title": "Language Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"type": "object", "additionalProperties": {"type": "string"}}, "title": "I18Dict"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-groups/": {"get": {"tags": ["admin"], "summary": "Get Milestone Groups Admin", "operationId": "get_milestone_groups_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}, "type": "array", "title": "Response Get Milestone Groups Admin Admin Milestone Groups Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create Milestone Group Admin", "operationId": "create_milestone_group_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestone-groups": {"put": {"tags": ["admin"], "summary": "Update Milestone Group Admin", "operationId": "update_milestone_group_admin", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestone-groups/{milestone_group_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone Group Admin", "operationId": "delete_milestone_group_admin", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-group-images/{milestone_group_id}": {"put": {"tags": ["admin"], "summary": "Upload Milestone Group Image", "operationId": "upload_milestone_group_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestones/{milestone_group_id}": {"post": {"tags": ["admin"], "summary": "Create Milestone", "operationId": "create_milestone", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestones/": {"put": {"tags": ["admin"], "summary": "Update Milestone", "operationId": "update_milestone", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestones/{milestone_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone", "operationId": "delete_milestone", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-images/{milestone_id}": {"post": {"tags": ["admin"], "summary": "Upload Milestone Image", "operationId": "upload_milestone_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_milestone_image_admin_milestone_images__milestone_id__post"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneImage"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-images/{milestone_image_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone Image", "operationId": "delete_milestone_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_image_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Image Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-age-scores/{milestone_id}": {"get": {"tags": ["admin"], "summary": "Get Milestone Age Scores", "operationId": "get_milestone_age_scores", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAgeScores"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/user-questions/": {"get": {"tags": ["admin"], "summary": "Get User Questions Admin", "operationId": "get_user_questions_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserQuestionAdmin"}, "type": "array", "title": "Response Get User Questions Admin Admin User Questions Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["admin"], "summary": "Update User Question", "operationId": "update_user_question", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create User Question", "operationId": "create_user_question", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/user-questions/{user_question_id}": {"delete": {"tags": ["admin"], "summary": "Delete User Question", "operationId": "delete_user_question", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "user_question_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "User Question Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/child-questions/": {"get": {"tags": ["admin"], "summary": "Get Child Questions Admin", "operationId": "get_child_questions_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildQuestionAdmin"}, "type": "array", "title": "Response Get Child Questions Admin Admin Child Questions Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["admin"], "summary": "Update Child Question", "operationId": "update_child_question", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create Child Question", "operationId": "create_child_question", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/child-questions/{child_question_id}": {"delete": {"tags": ["admin"], "summary": "Delete Child Question", "operationId": "delete_child_question", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_question_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Question Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/me": {"get": {"tags": ["users"], "summary": "Users:Current User", "operationId": "users:current_user", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}}, "security": [{"APIKeyCookie": []}]}, "patch": {"tags": ["users"], "summary": "Users:Patch Current User", "operationId": "users:patch_current_user", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserUpdate"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"UPDATE_USER_EMAIL_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"}}, "UPDATE_USER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/{id}": {"get": {"tags": ["users"], "summary": "Users:User", "operationId": "users:user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["users"], "summary": "Users:Patch User", "operationId": "users:patch_user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserUpdate"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "400": {"content": {"application/json": {"examples": {"UPDATE_USER_EMAIL_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"}}, "UPDATE_USER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}, "schema": {"$ref": "#/components/schemas/ErrorModel"}}}, "description": "Bad Request"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Users:Delete User", "operationId": "users:delete_user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "responses": {"204": {"description": "Successful Response"}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/children/": {"get": {"tags": ["users"], "summary": "Get Children", "operationId": "get_children", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildPublic"}, "type": "array", "title": "Response Get Children Users Children Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["users"], "summary": "Update Child", "operationId": "update_child", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["users"], "summary": "Create Child", "operationId": "create_child", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildCreate"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/children/{child_id}": {"get": {"tags": ["users"], "summary": "Get Child", "operationId": "get_child", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Delete Child", "operationId": "delete_child", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/children-images/{child_id}": {"get": {"tags": ["users"], "summary": "Get Child Image", "operationId": "get_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "put": {"tags": ["users"], "summary": "Upload Child Image", "operationId": "upload_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_child_image_users_children_images__child_id__put"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Delete Child Image", "operationId": "delete_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/milestone-answers/{child_id}": {"get": {"tags": ["users"], "summary": "Get Current Milestone Answer Session", "operationId": "get_current_milestone_answer_session", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerSessionPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/milestone-answers/{milestone_answer_session_id}": {"put": {"tags": ["users"], "summary": "Update Milestone Answer", "operationId": "update_milestone_answer", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_answer_session_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Answer Session Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/user-answers/": {"get": {"tags": ["users"], "summary": "Get Current User Answers", "operationId": "get_current_user_answers", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "Response Get Current User Answers Users User Answers Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["users"], "summary": "Update Current User Answers", "operationId": "update_current_user_answers", "requestBody": {"content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "New Answers"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "Response Update Current User Answers Users User Answers Put"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/children-answers/{child_id}": {"get": {"tags": ["users"], "summary": "Get Current Child Answers", "operationId": "get_current_child_answers", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/ChildAnswerPublic"}, "title": "Response Get Current Child Answers Users Children Answers Child Id Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "put": {"tags": ["users"], "summary": "Update Current Child Answers", "operationId": "update_current_child_answers", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/ChildAnswerPublic"}, "title": "New Answers"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/login": {"post": {"tags": ["auth"], "summary": "Auth:Cookie.Login", "operationId": "auth:cookie.login", "requestBody": {"content": {"application/x-www-form-urlencoded": {"schema": {"$ref": "#/components/schemas/Body_auth_cookie_login_auth_login_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"LOGIN_BAD_CREDENTIALS": {"summary": "Bad credentials or the user is inactive.", "value": {"detail": "LOGIN_BAD_CREDENTIALS"}}, "LOGIN_USER_NOT_VERIFIED": {"summary": "The user is not verified.", "value": {"detail": "LOGIN_USER_NOT_VERIFIED"}}}}}}, "204": {"description": "No Content"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/logout": {"post": {"tags": ["auth"], "summary": "Auth:Cookie.Logout", "operationId": "auth:cookie.logout", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "401": {"description": "Missing token or inactive user."}, "204": {"description": "No Content"}}, "security": [{"APIKeyCookie": []}]}}, "/auth/register": {"post": {"tags": ["auth"], "summary": "Register:Register", "operationId": "register:register", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserCreate"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"REGISTER_USER_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "REGISTER_USER_ALREADY_EXISTS"}}, "REGISTER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "REGISTER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/forgot-password": {"post": {"tags": ["auth"], "summary": "Reset:Forgot Password", "operationId": "reset:forgot_password", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_reset_forgot_password_auth_forgot_password_post"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/reset-password": {"post": {"tags": ["auth"], "summary": "Reset:Reset Password", "operationId": "reset:reset_password", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_reset_reset_password_auth_reset_password_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"RESET_PASSWORD_BAD_TOKEN": {"summary": "Bad or expired token.", "value": {"detail": "RESET_PASSWORD_BAD_TOKEN"}}, "RESET_PASSWORD_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "RESET_PASSWORD_INVALID_PASSWORD", "reason": "Password should be at least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/request-verify-token": {"post": {"tags": ["auth"], "summary": "Verify:Request-Token", "operationId": "verify:request-token", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_verify_request_token_auth_request_verify_token_post"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/verify": {"post": {"tags": ["auth"], "summary": "Verify:Verify", "operationId": "verify:verify", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_verify_verify_auth_verify_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"VERIFY_USER_BAD_TOKEN": {"summary": "Bad token, not existing user ornot the e-mail currently set for the user.", "value": {"detail": "VERIFY_USER_BAD_TOKEN"}}, "VERIFY_USER_ALREADY_VERIFIED": {"summary": "The user is already verified.", "value": {"detail": "VERIFY_USER_ALREADY_VERIFIED"}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/research/auth/": {"get": {"tags": ["research"], "summary": "Auth", "operationId": "auth", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}, "security": [{"APIKeyCookie": []}]}}}, "components": {"schemas": {"Body_auth_cookie_login_auth_login_post": {"properties": {"grant_type": {"anyOf": [{"type": "string", "pattern": "password"}, {"type": "null"}], "title": "Grant Type"}, "username": {"type": "string", "title": "Username"}, "password": {"type": "string", "title": "Password"}, "scope": {"type": "string", "title": "Scope", "default": ""}, "client_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Client Id"}, "client_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Client Secret"}}, "type": "object", "required": ["username", "password"], "title": "Body_auth_cookie_login_auth_login_post"}, "Body_reset_forgot_password_auth_forgot_password_post": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}}, "type": "object", "required": ["email"], "title": "Body_reset_forgot_password_auth_forgot_password_post"}, "Body_reset_reset_password_auth_reset_password_post": {"properties": {"token": {"type": "string", "title": "Token"}, "password": {"type": "string", "title": "Password"}}, "type": "object", "required": ["token", "password"], "title": "Body_reset_reset_password_auth_reset_password_post"}, "Body_upload_child_image_users_children_images__child_id__put": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_child_image_users_children_images__child_id__put"}, "Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put"}, "Body_upload_milestone_image_admin_milestone_images__milestone_id__post": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_milestone_image_admin_milestone_images__milestone_id__post"}, "Body_verify_request_token_auth_request_verify_token_post": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}}, "type": "object", "required": ["email"], "title": "Body_verify_request_token_auth_request_verify_token_post"}, "Body_verify_verify_auth_verify_post": {"properties": {"token": {"type": "string", "title": "Token"}}, "type": "object", "required": ["token"], "title": "Body_verify_verify_auth_verify_post"}, "ChildAnswerPublic": {"properties": {"answer": {"type": "string", "title": "Answer"}, "additional_answer": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Additional Answer"}, "question_id": {"type": "integer", "title": "Question Id"}}, "type": "object", "required": ["answer", "additional_answer", "question_id"], "title": "ChildAnswerPublic"}, "ChildCreate": {"properties": {"name": {"type": "string", "title": "Name", "default": ""}, "birth_year": {"type": "integer", "title": "Birth Year"}, "birth_month": {"type": "integer", "title": "Birth Month"}}, "type": "object", "required": ["birth_year", "birth_month"], "title": "ChildCreate"}, "ChildPublic": {"properties": {"name": {"type": "string", "title": "Name", "default": ""}, "birth_year": {"type": "integer", "title": "Birth Year"}, "birth_month": {"type": "integer", "title": "Birth Month"}, "id": {"type": "integer", "title": "Id"}, "has_image": {"type": "boolean", "title": "Has Image"}}, "type": "object", "required": ["birth_year", "birth_month", "id", "has_image"], "title": "ChildPublic"}, "ChildQuestionAdmin": {"properties": {"order": {"type": "integer", "title": "Order", "default": 0}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "options": {"type": "string", "title": "Options", "default": ""}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}, "id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/ChildQuestionText"}, "type": "object", "title": "Text", "default": {}}}, "type": "object", "required": ["id"], "title": "ChildQuestionAdmin"}, "ChildQuestionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/QuestionTextPublic"}, "type": "object", "title": "Text", "default": {}}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}}, "type": "object", "required": ["id"], "title": "ChildQuestionPublic"}, "ChildQuestionText": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}, "child_question_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Child Question Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "ChildQuestionText"}, "ErrorModel": {"properties": {"detail": {"anyOf": [{"type": "string"}, {"additionalProperties": {"type": "string"}, "type": "object"}], "title": "Detail"}}, "type": "object", "required": ["detail"], "title": "ErrorModel"}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "Language": {"properties": {"id": {"type": "string", "maxLength": 2, "title": "Id"}}, "type": "object", "required": ["id"], "title": "Language"}, "MilestoneAdmin": {"properties": {"id": {"type": "integer", "title": "Id"}, "group_id": {"type": "integer", "title": "Group Id"}, "order": {"type": "integer", "title": "Order"}, "expected_age_months": {"type": "integer", "title": "Expected Age Months"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneText"}, "type": "object", "title": "Text"}, "images": {"items": {"$ref": "#/components/schemas/MilestoneImage"}, "type": "array", "title": "Images"}}, "type": "object", "required": ["id", "group_id", "order", "expected_age_months", "text", "images"], "title": "MilestoneAdmin"}, "MilestoneAgeScore": {"properties": {"age_months": {"type": "integer", "title": "Age Months"}, "avg_score": {"type": "number", "title": "Avg Score"}, "expected_score": {"type": "number", "title": "Expected Score"}}, "type": "object", "required": ["age_months", "avg_score", "expected_score"], "title": "MilestoneAgeScore"}, "MilestoneAgeScores": {"properties": {"scores": {"items": {"$ref": "#/components/schemas/MilestoneAgeScore"}, "type": "array", "title": "Scores"}, "expected_age": {"type": "integer", "title": "Expected Age"}}, "type": "object", "required": ["scores", "expected_age"], "title": "MilestoneAgeScores"}, "MilestoneAnswerPublic": {"properties": {"milestone_id": {"type": "integer", "title": "Milestone Id"}, "answer": {"type": "integer", "title": "Answer"}}, "type": "object", "required": ["milestone_id", "answer"], "title": "MilestoneAnswerPublic"}, "MilestoneAnswerSessionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "child_id": {"type": "integer", "title": "Child Id"}, "created_at": {"type": "string", "format": "date-time", "title": "Created At"}, "answers": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}, "type": "object", "title": "Answers"}}, "type": "object", "required": ["id", "child_id", "created_at", "answers"], "title": "MilestoneAnswerSessionPublic"}, "MilestoneGroupAdmin": {"properties": {"id": {"type": "integer", "title": "Id"}, "order": {"type": "integer", "title": "Order"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneGroupText"}, "type": "object", "title": "Text"}, "milestones": {"items": {"$ref": "#/components/schemas/MilestoneAdmin"}, "type": "array", "title": "Milestones"}}, "type": "object", "required": ["id", "order", "text", "milestones"], "title": "MilestoneGroupAdmin"}, "MilestoneGroupPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneGroupTextPublic"}, "type": "object", "title": "Text"}, "milestones": {"items": {"$ref": "#/components/schemas/MilestonePublic"}, "type": "array", "title": "Milestones"}}, "type": "object", "required": ["id", "text", "milestones"], "title": "MilestoneGroupPublic"}, "MilestoneGroupText": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "group_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Group Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "MilestoneGroupText"}, "MilestoneGroupTextPublic": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}}, "type": "object", "title": "MilestoneGroupTextPublic"}, "MilestoneImage": {"properties": {"id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Id"}, "milestone_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Milestone Id"}, "filename": {"type": "string", "title": "Filename", "default": ""}, "approved": {"type": "boolean", "title": "Approved", "default": false}}, "type": "object", "title": "MilestoneImage"}, "MilestoneImagePublic": {"properties": {"filename": {"type": "string", "title": "Filename"}, "approved": {"type": "boolean", "title": "Approved"}}, "type": "object", "required": ["filename", "approved"], "title": "MilestoneImagePublic"}, "MilestonePublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "expected_age_months": {"type": "integer", "title": "Expected Age Months"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneTextPublic"}, "type": "object", "title": "Text"}, "images": {"items": {"$ref": "#/components/schemas/MilestoneImagePublic"}, "type": "array", "title": "Images"}}, "type": "object", "required": ["id", "expected_age_months", "text", "images"], "title": "MilestonePublic"}, "MilestoneText": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "obs": {"type": "string", "title": "Obs", "default": ""}, "help": {"type": "string", "title": "Help", "default": ""}, "milestone_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Milestone Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "MilestoneText"}, "MilestoneTextPublic": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "obs": {"type": "string", "title": "Obs", "default": ""}, "help": {"type": "string", "title": "Help", "default": ""}}, "type": "object", "title": "MilestoneTextPublic"}, "QuestionTextPublic": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}}, "type": "object", "title": "QuestionTextPublic"}, "UserAnswerPublic": {"properties": {"answer": {"type": "string", "title": "Answer"}, "additional_answer": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Additional Answer"}, "question_id": {"type": "integer", "title": "Question Id"}}, "type": "object", "required": ["answer", "additional_answer", "question_id"], "title": "UserAnswerPublic"}, "UserCreate": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}, "password": {"type": "string", "title": "Password"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active", "default": true}, "is_superuser": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Superuser", "default": false}, "is_verified": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Verified", "default": false}, "is_researcher": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Researcher", "default": false}}, "type": "object", "required": ["email", "password"], "title": "UserCreate"}, "UserQuestionAdmin": {"properties": {"order": {"type": "integer", "title": "Order", "default": 0}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "options": {"type": "string", "title": "Options", "default": ""}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}, "id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/UserQuestionText"}, "type": "object", "title": "Text", "default": {}}}, "type": "object", "required": ["id"], "title": "UserQuestionAdmin"}, "UserQuestionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/QuestionTextPublic"}, "type": "object", "title": "Text", "default": {}}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}}, "type": "object", "required": ["id"], "title": "UserQuestionPublic"}, "UserQuestionText": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}, "user_question_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "User Question Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "UserQuestionText"}, "UserRead": {"properties": {"id": {"type": "integer", "title": "Id"}, "email": {"type": "string", "format": "email", "title": "Email"}, "is_active": {"type": "boolean", "title": "Is Active", "default": true}, "is_superuser": {"type": "boolean", "title": "Is Superuser", "default": false}, "is_verified": {"type": "boolean", "title": "Is Verified", "default": false}, "is_researcher": {"type": "boolean", "title": "Is Researcher"}}, "type": "object", "required": ["id", "email", "is_researcher"], "title": "UserRead"}, "UserUpdate": {"properties": {"password": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Password"}, "email": {"anyOf": [{"type": "string", "format": "email"}, {"type": "null"}], "title": "Email"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active"}, "is_superuser": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Superuser"}, "is_verified": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Verified"}, "is_researcher": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Researcher"}}, "type": "object", "title": "UserUpdate"}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}}, "securitySchemes": {"APIKeyCookie": {"type": "apiKey", "in": "cookie", "name": "fastapiusersauth"}}}} \ No newline at end of file +{"openapi": "3.1.0", "info": {"title": "MONDEY API", "version": "0.1.0"}, "paths": {"/languages/": {"get": {"tags": ["milestones"], "summary": "Get Languages", "operationId": "get_languages", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"type": "string"}, "type": "array", "title": "Response Get Languages Languages Get"}}}}}}}, "/milestones/": {"get": {"tags": ["milestones"], "summary": "Get Milestones", "operationId": "get_milestones", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/MilestonePublic"}, "type": "array", "title": "Response Get Milestones Milestones Get"}}}}}}}, "/milestones/{milestone_id}": {"get": {"tags": ["milestones"], "summary": "Get Milestone", "operationId": "get_milestone", "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestonePublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/milestone-groups/{child_id}": {"get": {"tags": ["milestones"], "summary": "Get Milestone Groups", "operationId": "get_milestone_groups", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/MilestoneGroupPublic"}, "title": "Response Get Milestone Groups Milestone Groups Child Id Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/user-questions/": {"get": {"tags": ["questions"], "summary": "Get User Questions", "operationId": "get_user_questions", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserQuestionPublic"}, "type": "array", "title": "Response Get User Questions User Questions Get"}}}}}}}, "/child-questions/": {"get": {"tags": ["questions"], "summary": "Get Child Questions", "operationId": "get_child_questions", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildQuestionPublic"}, "type": "array", "title": "Response Get Child Questions Child Questions Get"}}}}}}}, "/admin/languages/": {"post": {"tags": ["admin"], "summary": "Create Language", "operationId": "create_language", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Language"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Language"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/languages/{language_id}": {"delete": {"tags": ["admin"], "summary": "Delete Language", "operationId": "delete_language", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "language_id", "in": "path", "required": true, "schema": {"type": "string", "title": "Language Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/i18n/{language_id}": {"put": {"tags": ["admin"], "summary": "Update I18N", "operationId": "update_i18n", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "language_id", "in": "path", "required": true, "schema": {"type": "string", "title": "Language Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"type": "object", "additionalProperties": {"type": "string"}}, "title": "I18Dict"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-groups/": {"get": {"tags": ["admin"], "summary": "Get Milestone Groups Admin", "operationId": "get_milestone_groups_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}, "type": "array", "title": "Response Get Milestone Groups Admin Admin Milestone Groups Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create Milestone Group Admin", "operationId": "create_milestone_group_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestone-groups": {"put": {"tags": ["admin"], "summary": "Update Milestone Group Admin", "operationId": "update_milestone_group_admin", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneGroupAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestone-groups/{milestone_group_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone Group Admin", "operationId": "delete_milestone_group_admin", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-group-images/{milestone_group_id}": {"put": {"tags": ["admin"], "summary": "Upload Milestone Group Image", "operationId": "upload_milestone_group_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestones/{milestone_group_id}": {"post": {"tags": ["admin"], "summary": "Create Milestone", "operationId": "create_milestone", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_group_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Group Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestones/": {"put": {"tags": ["admin"], "summary": "Update Milestone", "operationId": "update_milestone", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/milestones/{milestone_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone", "operationId": "delete_milestone", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-images/{milestone_id}": {"post": {"tags": ["admin"], "summary": "Upload Milestone Image", "operationId": "upload_milestone_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_milestone_image_admin_milestone_images__milestone_id__post"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneImage"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-images/{milestone_image_id}": {"delete": {"tags": ["admin"], "summary": "Delete Milestone Image", "operationId": "delete_milestone_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_image_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Image Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/milestone-age-scores/{milestone_id}": {"get": {"tags": ["admin"], "summary": "Get Milestone Age Scores", "operationId": "get_milestone_age_scores", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAgeScores"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/user-questions/": {"get": {"tags": ["admin"], "summary": "Get User Questions Admin", "operationId": "get_user_questions_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserQuestionAdmin"}, "type": "array", "title": "Response Get User Questions Admin Admin User Questions Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["admin"], "summary": "Update User Question", "operationId": "update_user_question", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create User Question", "operationId": "create_user_question", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserQuestionAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/user-questions/{user_question_id}": {"delete": {"tags": ["admin"], "summary": "Delete User Question", "operationId": "delete_user_question", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "user_question_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "User Question Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/child-questions/": {"get": {"tags": ["admin"], "summary": "Get Child Questions Admin", "operationId": "get_child_questions_admin", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildQuestionAdmin"}, "type": "array", "title": "Response Get Child Questions Admin Admin Child Questions Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["admin"], "summary": "Update Child Question", "operationId": "update_child_question", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["admin"], "summary": "Create Child Question", "operationId": "create_child_question", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildQuestionAdmin"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/admin/child-questions/{child_question_id}": {"delete": {"tags": ["admin"], "summary": "Delete Child Question", "operationId": "delete_child_question", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_question_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Question Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/admin/users/": {"get": {"tags": ["admin"], "summary": "Get Users", "operationId": "get_users", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserRead"}, "type": "array", "title": "Response Get Users Admin Users Get"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/me": {"get": {"tags": ["users"], "summary": "Users:Current User", "operationId": "users:current_user", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}}, "security": [{"APIKeyCookie": []}]}, "patch": {"tags": ["users"], "summary": "Users:Patch Current User", "operationId": "users:patch_current_user", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserUpdate"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"UPDATE_USER_EMAIL_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"}}, "UPDATE_USER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/{id}": {"get": {"tags": ["users"], "summary": "Users:User", "operationId": "users:user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["users"], "summary": "Users:Patch User", "operationId": "users:patch_user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserUpdate"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "400": {"content": {"application/json": {"examples": {"UPDATE_USER_EMAIL_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"}}, "UPDATE_USER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}, "schema": {"$ref": "#/components/schemas/ErrorModel"}}}, "description": "Bad Request"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Users:Delete User", "operationId": "users:delete_user", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "title": "Id"}}], "responses": {"204": {"description": "Successful Response"}, "401": {"description": "Missing token or inactive user."}, "403": {"description": "Not a superuser."}, "404": {"description": "The user does not exist."}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/children/": {"get": {"tags": ["users"], "summary": "Get Children", "operationId": "get_children", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/ChildPublic"}, "type": "array", "title": "Response Get Children Users Children Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["users"], "summary": "Update Child", "operationId": "update_child", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}, "post": {"tags": ["users"], "summary": "Create Child", "operationId": "create_child", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildCreate"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/children/{child_id}": {"get": {"tags": ["users"], "summary": "Get Child", "operationId": "get_child", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChildPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Delete Child", "operationId": "delete_child", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/children-images/{child_id}": {"get": {"tags": ["users"], "summary": "Get Child Image", "operationId": "get_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "put": {"tags": ["users"], "summary": "Upload Child Image", "operationId": "upload_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_child_image_users_children_images__child_id__put"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["users"], "summary": "Delete Child Image", "operationId": "delete_child_image", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/milestone-answers/{child_id}": {"get": {"tags": ["users"], "summary": "Get Current Milestone Answer Session", "operationId": "get_current_milestone_answer_session", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerSessionPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/milestone-answers/{milestone_answer_session_id}": {"put": {"tags": ["users"], "summary": "Update Milestone Answer", "operationId": "update_milestone_answer", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "milestone_answer_session_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Milestone Answer Session Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/users/user-answers/": {"get": {"tags": ["users"], "summary": "Get Current User Answers", "operationId": "get_current_user_answers", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "Response Get Current User Answers Users User Answers Get"}}}}}, "security": [{"APIKeyCookie": []}]}, "put": {"tags": ["users"], "summary": "Update Current User Answers", "operationId": "update_current_user_answers", "requestBody": {"content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "New Answers"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/UserAnswerPublic"}, "type": "array", "title": "Response Update Current User Answers Users User Answers Put"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyCookie": []}]}}, "/users/children-answers/{child_id}": {"get": {"tags": ["users"], "summary": "Get Current Child Answers", "operationId": "get_current_child_answers", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/ChildAnswerPublic"}, "title": "Response Get Current Child Answers Users Children Answers Child Id Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "put": {"tags": ["users"], "summary": "Update Current Child Answers", "operationId": "update_current_child_answers", "security": [{"APIKeyCookie": []}], "parameters": [{"name": "child_id", "in": "path", "required": true, "schema": {"type": "integer", "title": "Child Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/ChildAnswerPublic"}, "title": "New Answers"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/login": {"post": {"tags": ["auth"], "summary": "Auth:Cookie.Login", "operationId": "auth:cookie.login", "requestBody": {"content": {"application/x-www-form-urlencoded": {"schema": {"$ref": "#/components/schemas/Body_auth_cookie_login_auth_login_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"LOGIN_BAD_CREDENTIALS": {"summary": "Bad credentials or the user is inactive.", "value": {"detail": "LOGIN_BAD_CREDENTIALS"}}, "LOGIN_USER_NOT_VERIFIED": {"summary": "The user is not verified.", "value": {"detail": "LOGIN_USER_NOT_VERIFIED"}}}}}}, "204": {"description": "No Content"}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/logout": {"post": {"tags": ["auth"], "summary": "Auth:Cookie.Logout", "operationId": "auth:cookie.logout", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "401": {"description": "Missing token or inactive user."}, "204": {"description": "No Content"}}, "security": [{"APIKeyCookie": []}]}}, "/auth/register": {"post": {"tags": ["auth"], "summary": "Register:Register", "operationId": "register:register", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserCreate"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"REGISTER_USER_ALREADY_EXISTS": {"summary": "A user with this email already exists.", "value": {"detail": "REGISTER_USER_ALREADY_EXISTS"}}, "REGISTER_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "REGISTER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/forgot-password": {"post": {"tags": ["auth"], "summary": "Reset:Forgot Password", "operationId": "reset:forgot_password", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_reset_forgot_password_auth_forgot_password_post"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/reset-password": {"post": {"tags": ["auth"], "summary": "Reset:Reset Password", "operationId": "reset:reset_password", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_reset_reset_password_auth_reset_password_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"RESET_PASSWORD_BAD_TOKEN": {"summary": "Bad or expired token.", "value": {"detail": "RESET_PASSWORD_BAD_TOKEN"}}, "RESET_PASSWORD_INVALID_PASSWORD": {"summary": "Password validation failed.", "value": {"detail": {"code": "RESET_PASSWORD_INVALID_PASSWORD", "reason": "Password should be at least 3 characters"}}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/request-verify-token": {"post": {"tags": ["auth"], "summary": "Verify:Request-Token", "operationId": "verify:request-token", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_verify_request_token_auth_request_verify_token_post"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/auth/verify": {"post": {"tags": ["auth"], "summary": "Verify:Verify", "operationId": "verify:verify", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Body_verify_verify_auth_verify_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRead"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorModel"}, "examples": {"VERIFY_USER_BAD_TOKEN": {"summary": "Bad token, not existing user ornot the e-mail currently set for the user.", "value": {"detail": "VERIFY_USER_BAD_TOKEN"}}, "VERIFY_USER_ALREADY_VERIFIED": {"summary": "The user is already verified.", "value": {"detail": "VERIFY_USER_ALREADY_VERIFIED"}}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/research/auth/": {"get": {"tags": ["research"], "summary": "Auth", "operationId": "auth", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}, "security": [{"APIKeyCookie": []}]}}}, "components": {"schemas": {"Body_auth_cookie_login_auth_login_post": {"properties": {"grant_type": {"anyOf": [{"type": "string", "pattern": "password"}, {"type": "null"}], "title": "Grant Type"}, "username": {"type": "string", "title": "Username"}, "password": {"type": "string", "title": "Password"}, "scope": {"type": "string", "title": "Scope", "default": ""}, "client_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Client Id"}, "client_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Client Secret"}}, "type": "object", "required": ["username", "password"], "title": "Body_auth_cookie_login_auth_login_post"}, "Body_reset_forgot_password_auth_forgot_password_post": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}}, "type": "object", "required": ["email"], "title": "Body_reset_forgot_password_auth_forgot_password_post"}, "Body_reset_reset_password_auth_reset_password_post": {"properties": {"token": {"type": "string", "title": "Token"}, "password": {"type": "string", "title": "Password"}}, "type": "object", "required": ["token", "password"], "title": "Body_reset_reset_password_auth_reset_password_post"}, "Body_upload_child_image_users_children_images__child_id__put": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_child_image_users_children_images__child_id__put"}, "Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_milestone_group_image_admin_milestone_group_images__milestone_group_id__put"}, "Body_upload_milestone_image_admin_milestone_images__milestone_id__post": {"properties": {"file": {"type": "string", "format": "binary", "title": "File"}}, "type": "object", "required": ["file"], "title": "Body_upload_milestone_image_admin_milestone_images__milestone_id__post"}, "Body_verify_request_token_auth_request_verify_token_post": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}}, "type": "object", "required": ["email"], "title": "Body_verify_request_token_auth_request_verify_token_post"}, "Body_verify_verify_auth_verify_post": {"properties": {"token": {"type": "string", "title": "Token"}}, "type": "object", "required": ["token"], "title": "Body_verify_verify_auth_verify_post"}, "ChildAnswerPublic": {"properties": {"answer": {"type": "string", "title": "Answer"}, "additional_answer": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Additional Answer"}, "question_id": {"type": "integer", "title": "Question Id"}}, "type": "object", "required": ["answer", "additional_answer", "question_id"], "title": "ChildAnswerPublic"}, "ChildCreate": {"properties": {"name": {"type": "string", "title": "Name", "default": ""}, "birth_year": {"type": "integer", "title": "Birth Year"}, "birth_month": {"type": "integer", "title": "Birth Month"}}, "type": "object", "required": ["birth_year", "birth_month"], "title": "ChildCreate"}, "ChildPublic": {"properties": {"name": {"type": "string", "title": "Name", "default": ""}, "birth_year": {"type": "integer", "title": "Birth Year"}, "birth_month": {"type": "integer", "title": "Birth Month"}, "id": {"type": "integer", "title": "Id"}, "has_image": {"type": "boolean", "title": "Has Image"}}, "type": "object", "required": ["birth_year", "birth_month", "id", "has_image"], "title": "ChildPublic"}, "ChildQuestionAdmin": {"properties": {"order": {"type": "integer", "title": "Order", "default": 0}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "options": {"type": "string", "title": "Options", "default": ""}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}, "id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/ChildQuestionText"}, "type": "object", "title": "Text", "default": {}}}, "type": "object", "required": ["id"], "title": "ChildQuestionAdmin"}, "ChildQuestionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/QuestionTextPublic"}, "type": "object", "title": "Text", "default": {}}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}}, "type": "object", "required": ["id"], "title": "ChildQuestionPublic"}, "ChildQuestionText": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}, "child_question_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Child Question Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "ChildQuestionText"}, "ErrorModel": {"properties": {"detail": {"anyOf": [{"type": "string"}, {"additionalProperties": {"type": "string"}, "type": "object"}], "title": "Detail"}}, "type": "object", "required": ["detail"], "title": "ErrorModel"}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "Language": {"properties": {"id": {"type": "string", "maxLength": 2, "title": "Id"}}, "type": "object", "required": ["id"], "title": "Language"}, "MilestoneAdmin": {"properties": {"id": {"type": "integer", "title": "Id"}, "group_id": {"type": "integer", "title": "Group Id"}, "order": {"type": "integer", "title": "Order"}, "expected_age_months": {"type": "integer", "title": "Expected Age Months"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneText"}, "type": "object", "title": "Text"}, "images": {"items": {"$ref": "#/components/schemas/MilestoneImage"}, "type": "array", "title": "Images"}}, "type": "object", "required": ["id", "group_id", "order", "expected_age_months", "text", "images"], "title": "MilestoneAdmin"}, "MilestoneAgeScore": {"properties": {"age_months": {"type": "integer", "title": "Age Months"}, "avg_score": {"type": "number", "title": "Avg Score"}, "expected_score": {"type": "number", "title": "Expected Score"}}, "type": "object", "required": ["age_months", "avg_score", "expected_score"], "title": "MilestoneAgeScore"}, "MilestoneAgeScores": {"properties": {"scores": {"items": {"$ref": "#/components/schemas/MilestoneAgeScore"}, "type": "array", "title": "Scores"}, "expected_age": {"type": "integer", "title": "Expected Age"}}, "type": "object", "required": ["scores", "expected_age"], "title": "MilestoneAgeScores"}, "MilestoneAnswerPublic": {"properties": {"milestone_id": {"type": "integer", "title": "Milestone Id"}, "answer": {"type": "integer", "title": "Answer"}}, "type": "object", "required": ["milestone_id", "answer"], "title": "MilestoneAnswerPublic"}, "MilestoneAnswerSessionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "child_id": {"type": "integer", "title": "Child Id"}, "created_at": {"type": "string", "format": "date-time", "title": "Created At"}, "answers": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneAnswerPublic"}, "type": "object", "title": "Answers"}}, "type": "object", "required": ["id", "child_id", "created_at", "answers"], "title": "MilestoneAnswerSessionPublic"}, "MilestoneGroupAdmin": {"properties": {"id": {"type": "integer", "title": "Id"}, "order": {"type": "integer", "title": "Order"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneGroupText"}, "type": "object", "title": "Text"}, "milestones": {"items": {"$ref": "#/components/schemas/MilestoneAdmin"}, "type": "array", "title": "Milestones"}}, "type": "object", "required": ["id", "order", "text", "milestones"], "title": "MilestoneGroupAdmin"}, "MilestoneGroupPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneGroupTextPublic"}, "type": "object", "title": "Text"}, "milestones": {"items": {"$ref": "#/components/schemas/MilestonePublic"}, "type": "array", "title": "Milestones"}}, "type": "object", "required": ["id", "text", "milestones"], "title": "MilestoneGroupPublic"}, "MilestoneGroupText": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "group_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Group Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "MilestoneGroupText"}, "MilestoneGroupTextPublic": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}}, "type": "object", "title": "MilestoneGroupTextPublic"}, "MilestoneImage": {"properties": {"id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Id"}, "milestone_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Milestone Id"}, "filename": {"type": "string", "title": "Filename", "default": ""}, "approved": {"type": "boolean", "title": "Approved", "default": false}}, "type": "object", "title": "MilestoneImage"}, "MilestoneImagePublic": {"properties": {"filename": {"type": "string", "title": "Filename"}, "approved": {"type": "boolean", "title": "Approved"}}, "type": "object", "required": ["filename", "approved"], "title": "MilestoneImagePublic"}, "MilestonePublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "expected_age_months": {"type": "integer", "title": "Expected Age Months"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/MilestoneTextPublic"}, "type": "object", "title": "Text"}, "images": {"items": {"$ref": "#/components/schemas/MilestoneImagePublic"}, "type": "array", "title": "Images"}}, "type": "object", "required": ["id", "expected_age_months", "text", "images"], "title": "MilestonePublic"}, "MilestoneText": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "obs": {"type": "string", "title": "Obs", "default": ""}, "help": {"type": "string", "title": "Help", "default": ""}, "milestone_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Milestone Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "MilestoneText"}, "MilestoneTextPublic": {"properties": {"title": {"type": "string", "title": "Title", "default": ""}, "desc": {"type": "string", "title": "Desc", "default": ""}, "obs": {"type": "string", "title": "Obs", "default": ""}, "help": {"type": "string", "title": "Help", "default": ""}}, "type": "object", "title": "MilestoneTextPublic"}, "QuestionTextPublic": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}}, "type": "object", "title": "QuestionTextPublic"}, "UserAnswerPublic": {"properties": {"answer": {"type": "string", "title": "Answer"}, "additional_answer": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Additional Answer"}, "question_id": {"type": "integer", "title": "Question Id"}}, "type": "object", "required": ["answer", "additional_answer", "question_id"], "title": "UserAnswerPublic"}, "UserCreate": {"properties": {"email": {"type": "string", "format": "email", "title": "Email"}, "password": {"type": "string", "title": "Password"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active", "default": true}, "is_superuser": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Superuser", "default": false}, "is_verified": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Verified", "default": false}, "is_researcher": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Researcher", "default": false}}, "type": "object", "required": ["email", "password"], "title": "UserCreate"}, "UserQuestionAdmin": {"properties": {"order": {"type": "integer", "title": "Order", "default": 0}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "options": {"type": "string", "title": "Options", "default": ""}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}, "id": {"type": "integer", "title": "Id"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/UserQuestionText"}, "type": "object", "title": "Text", "default": {}}}, "type": "object", "required": ["id"], "title": "UserQuestionAdmin"}, "UserQuestionPublic": {"properties": {"id": {"type": "integer", "title": "Id"}, "component": {"type": "string", "title": "Component", "default": "select"}, "type": {"type": "string", "title": "Type", "default": "text"}, "text": {"additionalProperties": {"$ref": "#/components/schemas/QuestionTextPublic"}, "type": "object", "title": "Text", "default": {}}, "additional_option": {"type": "string", "title": "Additional Option", "default": ""}}, "type": "object", "required": ["id"], "title": "UserQuestionPublic"}, "UserQuestionText": {"properties": {"question": {"type": "string", "title": "Question", "default": ""}, "options_json": {"type": "string", "title": "Options Json", "default": ""}, "options": {"type": "string", "title": "Options", "default": ""}, "user_question_id": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "User Question Id"}, "lang_id": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "null"}], "title": "Lang Id"}}, "type": "object", "title": "UserQuestionText"}, "UserRead": {"properties": {"id": {"type": "integer", "title": "Id"}, "email": {"type": "string", "format": "email", "title": "Email"}, "is_active": {"type": "boolean", "title": "Is Active", "default": true}, "is_superuser": {"type": "boolean", "title": "Is Superuser", "default": false}, "is_verified": {"type": "boolean", "title": "Is Verified", "default": false}, "is_researcher": {"type": "boolean", "title": "Is Researcher"}}, "type": "object", "required": ["id", "email", "is_researcher"], "title": "UserRead"}, "UserUpdate": {"properties": {"password": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Password"}, "email": {"anyOf": [{"type": "string", "format": "email"}, {"type": "null"}], "title": "Email"}, "is_active": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Active"}, "is_superuser": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Superuser"}, "is_verified": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Verified"}, "is_researcher": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "title": "Is Researcher"}}, "type": "object", "title": "UserUpdate"}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}}, "securitySchemes": {"APIKeyCookie": {"type": "apiKey", "in": "cookie", "name": "fastapiusersauth"}}}} \ No newline at end of file diff --git a/mondey_backend/pyproject.toml b/mondey_backend/pyproject.toml index 56e26bae..f1dd92a3 100644 --- a/mondey_backend/pyproject.toml +++ b/mondey_backend/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -tests = ["pytest", "pytest-randomly", "pytest-cov", "pytest-mock"] +tests = ["pytest", "pytest-randomly", "pytest-cov", "pytest-mock", "pytest-asyncio"] [project.scripts] mondey-backend = "mondey_backend.main:main" @@ -34,6 +34,7 @@ path = "src/mondey_backend/__init__.py" [tool.pytest.ini_options] testpaths = ["tests"] +asyncio_default_fixture_loop_scope = "function" [tool.ruff.lint] select = [ diff --git a/mondey_backend/src/mondey_backend/dependencies.py b/mondey_backend/src/mondey_backend/dependencies.py index ed2a5e85..2458d1d0 100644 --- a/mondey_backend/src/mondey_backend/dependencies.py +++ b/mondey_backend/src/mondey_backend/dependencies.py @@ -4,9 +4,11 @@ from fastapi import Depends from fastapi import HTTPException +from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import Session from .databases.milestones import engine as milestones_engine +from .databases.users import get_async_session from .models.users import User from .users import fastapi_users @@ -22,6 +24,8 @@ def get_session(): CurrentActiveUserDep = Annotated[User, Depends(current_active_user)] +UserAsyncSessionDep = Annotated[AsyncSession, Depends(get_async_session)] + def current_active_researcher( user: Annotated[User, Depends(current_active_user)], diff --git a/mondey_backend/src/mondey_backend/routers/admin.py b/mondey_backend/src/mondey_backend/routers/admin.py index caf5ffa0..07f8ebe0 100644 --- a/mondey_backend/src/mondey_backend/routers/admin.py +++ b/mondey_backend/src/mondey_backend/routers/admin.py @@ -1,270 +1,18 @@ from __future__ import annotations -import json -import pathlib - from fastapi import APIRouter -from fastapi import HTTPException -from fastapi import UploadFile -from sqlmodel import col -from sqlmodel import select from ..dependencies import AdminDep -from ..dependencies import SessionDep -from ..models.milestones import Language -from ..models.milestones import Milestone -from ..models.milestones import MilestoneAdmin -from ..models.milestones import MilestoneAgeScores -from ..models.milestones import MilestoneGroup -from ..models.milestones import MilestoneGroupAdmin -from ..models.milestones import MilestoneGroupText -from ..models.milestones import MilestoneImage -from ..models.milestones import MilestoneText -from ..models.questions import ChildQuestion -from ..models.questions import ChildQuestionAdmin -from ..models.questions import ChildQuestionText -from ..models.questions import UserQuestion -from ..models.questions import UserQuestionAdmin -from ..models.questions import UserQuestionText -from ..settings import app_settings -from .utils import add -from .utils import calculate_milestone_age_scores -from .utils import get -from .utils import update_child_question_text -from .utils import update_milestone_group_text -from .utils import update_milestone_text -from .utils import update_user_question_text -from .utils import write_file +from .admin_routers import languages +from .admin_routers import milestones +from .admin_routers import questions +from .admin_routers import users def create_router() -> APIRouter: router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[AdminDep]) - - @router.post("/languages/", response_model=Language) - def create_language(session: SessionDep, language: Language): - db_language = Language.model_validate(language) - if session.get(Language, db_language.id) is not None: - raise HTTPException(400, "Language already exists") - add(session, db_language) - return db_language - - @router.delete("/languages/{language_id}") - def delete_language(session: SessionDep, language_id: str): - if language_id in ["de", "en"]: - raise HTTPException( - status_code=400, detail=f"{language_id} language cannot be deleted" - ) - language = get(session, Language, language_id) - session.delete(language) - session.commit() - return {"ok": True} - - @router.put("/i18n/{language_id}") - async def update_i18n( - session: SessionDep, language_id: str, i18dict: dict[str, dict[str, str]] - ): - language = get(session, Language, language_id) - i18json_path = ( - pathlib.Path(app_settings.STATIC_FILES_PATH) - / "i18n" - / f"{language.id}.json" - ) - i18json_path.parent.mkdir(exist_ok=True) - with open(i18json_path, "w", encoding="utf-8") as i18json_file: - json.dump(i18dict, i18json_file, separators=(",", ":"), ensure_ascii=False) - return {"ok": True} - - @router.get("/milestone-groups/", response_model=list[MilestoneGroupAdmin]) - def get_milestone_groups_admin(session: SessionDep): - milestone_groups = session.exec( - select(MilestoneGroup).order_by(col(MilestoneGroup.order)) - ).all() - return milestone_groups - - @router.post("/milestone-groups/", response_model=MilestoneGroupAdmin) - def create_milestone_group_admin(session: SessionDep): - db_milestone_group = MilestoneGroup() - add(session, db_milestone_group) - for language in session.exec(select(Language)).all(): - session.add( - MilestoneGroupText(group_id=db_milestone_group.id, lang_id=language.id) - ) - session.commit() - session.refresh(db_milestone_group) - return db_milestone_group - - @router.put("/milestone-groups", response_model=MilestoneGroupAdmin) - def update_milestone_group_admin( - session: SessionDep, - milestone_group: MilestoneGroupAdmin, - ): - db_milestone_group = get(session, MilestoneGroup, milestone_group.id) - for key, value in milestone_group.model_dump( - exclude={"text", "milestones"} - ).items(): - setattr(db_milestone_group, key, value) - update_milestone_group_text(session, milestone_group) - add(session, db_milestone_group) - return db_milestone_group - - @router.delete("/milestone-groups/{milestone_group_id}") - def delete_milestone_group_admin(session: SessionDep, milestone_group_id: int): - milestone_group = get(session, MilestoneGroup, milestone_group_id) - session.delete(milestone_group) - session.commit() - return {"ok": True} - - @router.put("/milestone-group-images/{milestone_group_id}") - async def upload_milestone_group_image( - session: SessionDep, milestone_group_id: int, file: UploadFile - ): - milestone_group = get(session, MilestoneGroup, milestone_group_id) - filename = f"{app_settings.STATIC_FILES_PATH}/mg{milestone_group.id}.jpg" - write_file(file, filename) - return {"filename": filename} - - @router.post("/milestones/{milestone_group_id}", response_model=MilestoneAdmin) - def create_milestone(session: SessionDep, milestone_group_id: int): - db_milestone = Milestone(group_id=milestone_group_id) - add(session, db_milestone) - for language in session.exec(select(Language)).all(): - session.add( - MilestoneText(milestone_id=db_milestone.id, lang_id=language.id) - ) - session.commit() - session.refresh(db_milestone) - return db_milestone - - @router.put("/milestones/", response_model=MilestoneAdmin) - def update_milestone( - session: SessionDep, - milestone: MilestoneAdmin, - ): - db_milestone = get(session, Milestone, milestone.id) - for key, value in milestone.model_dump(exclude={"text", "images"}).items(): - setattr(db_milestone, key, value) - update_milestone_text(session, milestone) - add(session, db_milestone) - return db_milestone - - @router.delete("/milestones/{milestone_id}") - def delete_milestone(session: SessionDep, milestone_id: int): - milestone = get(session, Milestone, milestone_id) - session.delete(milestone) - session.commit() - return {"ok": True} - - @router.post("/milestone-images/{milestone_id}", response_model=MilestoneImage) - async def upload_milestone_image( - session: SessionDep, milestone_id: int, file: UploadFile - ): - milestone = get(session, Milestone, milestone_id) - milestone_image = MilestoneImage(milestone_id=milestone.id, approved=True) - add(session, milestone_image) - filename = f"m{milestone_image.id}.jpg" - milestone_image.filename = filename - write_file(file, f"{app_settings.STATIC_FILES_PATH}/{filename}") - session.commit() - session.refresh(milestone_image) - return milestone_image - - @router.delete("/milestone-images/{milestone_image_id}") - async def delete_milestone_image(session: SessionDep, milestone_image_id: int): - milestone_image = get(session, MilestoneImage, milestone_image_id) - pathlib.Path( - f"{app_settings.STATIC_FILES_PATH}/m{milestone_image.id}.jpg" - ).unlink(missing_ok=True) - session.delete(milestone_image) - session.commit() - return {"ok": True} - - @router.get("/milestone-age-scores/{milestone_id}") - def get_milestone_age_scores( - session: SessionDep, milestone_id: int - ) -> MilestoneAgeScores: - return calculate_milestone_age_scores(session, milestone_id) - - # User question CRUD endpoints - @router.get("/user-questions/", response_model=list[UserQuestionAdmin]) - def get_user_questions_admin(session: SessionDep): - user_questions = session.exec( - select(UserQuestion).order_by(col(UserQuestion.order)) - ).all() - return user_questions - - @router.post("/user-questions/", response_model=UserQuestionAdmin) - def create_user_question(session: SessionDep): - user_question = UserQuestion() - add(session, user_question) - for language in session.exec(select(Language)).all(): - session.add( - UserQuestionText(user_question_id=user_question.id, lang_id=language.id) - ) - session.commit() - session.refresh(user_question) - return user_question - - @router.put("/user-questions/", response_model=UserQuestionAdmin) - def update_user_question( - session: SessionDep, - user_question: UserQuestionAdmin, - ): - db_user_question = get(session, UserQuestion, user_question.id) - for key, value in user_question.model_dump(exclude={"text"}).items(): - setattr(db_user_question, key, value) - update_user_question_text(session, user_question) - add(session, db_user_question) - return db_user_question - - @router.delete("/user-questions/{user_question_id}") - def delete_user_question(session: SessionDep, user_question_id: int): - question = get(session, UserQuestion, user_question_id) - session.delete(question) - session.commit() - return {"ok": True} - - # Child question CRUD endpoints - @router.get("/child-questions/", response_model=list[ChildQuestionAdmin]) - def get_child_questions_admin(session: SessionDep): - user_questions = session.exec( - select(ChildQuestion).order_by(col(ChildQuestion.order)) - ).all() - return user_questions - - @router.post("/child-questions/", response_model=ChildQuestionAdmin) - def create_child_question( - session: SessionDep, - ): - child_question = ChildQuestion() - add(session, child_question) - for language in session.exec(select(Language)).all(): - session.add( - ChildQuestionText( - child_question_id=child_question.id, lang_id=language.id - ) - ) - session.commit() - session.refresh(child_question) - return child_question - - @router.put("/child-questions/", response_model=ChildQuestionAdmin) - def update_child_question( - session: SessionDep, - child_question: ChildQuestionAdmin, - ): - db_child_question = get(session, ChildQuestion, child_question.id) - - for key, value in child_question.model_dump(exclude={"text"}).items(): - setattr(db_child_question, key, value) - update_child_question_text(session, child_question) - add(session, db_child_question) - return db_child_question - - @router.delete("/child-questions/{child_question_id}") - def delete_child_question(session: SessionDep, child_question_id: int): - question = get(session, ChildQuestion, child_question_id) - session.delete(question) - session.commit() - return {"ok": True} - + router.include_router(languages.create_router()) + router.include_router(milestones.create_router()) + router.include_router(questions.create_router()) + router.include_router(users.create_router()) return router diff --git a/mondey_backend/src/mondey_backend/routers/admin_routers/__init__.py b/mondey_backend/src/mondey_backend/routers/admin_routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mondey_backend/src/mondey_backend/routers/admin_routers/languages.py b/mondey_backend/src/mondey_backend/routers/admin_routers/languages.py new file mode 100644 index 00000000..24a0f5fc --- /dev/null +++ b/mondey_backend/src/mondey_backend/routers/admin_routers/languages.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import json +import pathlib + +from fastapi import APIRouter +from fastapi import HTTPException + +from ...dependencies import SessionDep +from ...models.milestones import Language +from ...settings import app_settings +from ..utils import add +from ..utils import get + + +def create_router() -> APIRouter: + router = APIRouter() + + @router.post("/languages/", response_model=Language) + def create_language(session: SessionDep, language: Language): + db_language = Language.model_validate(language) + if session.get(Language, db_language.id) is not None: + raise HTTPException(400, "Language already exists") + add(session, db_language) + return db_language + + @router.delete("/languages/{language_id}") + def delete_language(session: SessionDep, language_id: str): + if language_id in ["de", "en"]: + raise HTTPException( + status_code=400, detail=f"{language_id} language cannot be deleted" + ) + language = get(session, Language, language_id) + session.delete(language) + session.commit() + return {"ok": True} + + @router.put("/i18n/{language_id}") + async def update_i18n( + session: SessionDep, language_id: str, i18dict: dict[str, dict[str, str]] + ): + language = get(session, Language, language_id) + i18json_path = ( + pathlib.Path(app_settings.STATIC_FILES_PATH) + / "i18n" + / f"{language.id}.json" + ) + i18json_path.parent.mkdir(exist_ok=True) + with open(i18json_path, "w", encoding="utf-8") as i18json_file: + json.dump(i18dict, i18json_file, separators=(",", ":"), ensure_ascii=False) + return {"ok": True} + + return router diff --git a/mondey_backend/src/mondey_backend/routers/admin_routers/milestones.py b/mondey_backend/src/mondey_backend/routers/admin_routers/milestones.py new file mode 100644 index 00000000..71026483 --- /dev/null +++ b/mondey_backend/src/mondey_backend/routers/admin_routers/milestones.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import pathlib + +from fastapi import APIRouter +from fastapi import UploadFile +from sqlmodel import col +from sqlmodel import select + +from ...dependencies import SessionDep +from ...models.milestones import Language +from ...models.milestones import Milestone +from ...models.milestones import MilestoneAdmin +from ...models.milestones import MilestoneAgeScores +from ...models.milestones import MilestoneGroup +from ...models.milestones import MilestoneGroupAdmin +from ...models.milestones import MilestoneGroupText +from ...models.milestones import MilestoneImage +from ...models.milestones import MilestoneText +from ...settings import app_settings +from ..utils import add +from ..utils import calculate_milestone_age_scores +from ..utils import get +from ..utils import update_milestone_group_text +from ..utils import update_milestone_text +from ..utils import write_file + + +def create_router() -> APIRouter: + router = APIRouter() + + @router.get("/milestone-groups/", response_model=list[MilestoneGroupAdmin]) + def get_milestone_groups_admin(session: SessionDep): + milestone_groups = session.exec( + select(MilestoneGroup).order_by(col(MilestoneGroup.order)) + ).all() + return milestone_groups + + @router.post("/milestone-groups/", response_model=MilestoneGroupAdmin) + def create_milestone_group_admin(session: SessionDep): + db_milestone_group = MilestoneGroup() + add(session, db_milestone_group) + for language in session.exec(select(Language)).all(): + session.add( + MilestoneGroupText(group_id=db_milestone_group.id, lang_id=language.id) + ) + session.commit() + session.refresh(db_milestone_group) + return db_milestone_group + + @router.put("/milestone-groups", response_model=MilestoneGroupAdmin) + def update_milestone_group_admin( + session: SessionDep, + milestone_group: MilestoneGroupAdmin, + ): + db_milestone_group = get(session, MilestoneGroup, milestone_group.id) + for key, value in milestone_group.model_dump( + exclude={"text", "milestones"} + ).items(): + setattr(db_milestone_group, key, value) + update_milestone_group_text(session, milestone_group) + add(session, db_milestone_group) + return db_milestone_group + + @router.delete("/milestone-groups/{milestone_group_id}") + def delete_milestone_group_admin(session: SessionDep, milestone_group_id: int): + milestone_group = get(session, MilestoneGroup, milestone_group_id) + session.delete(milestone_group) + session.commit() + return {"ok": True} + + @router.put("/milestone-group-images/{milestone_group_id}") + async def upload_milestone_group_image( + session: SessionDep, milestone_group_id: int, file: UploadFile + ): + milestone_group = get(session, MilestoneGroup, milestone_group_id) + filename = f"{app_settings.STATIC_FILES_PATH}/mg{milestone_group.id}.jpg" + write_file(file, filename) + return {"filename": filename} + + @router.post("/milestones/{milestone_group_id}", response_model=MilestoneAdmin) + def create_milestone(session: SessionDep, milestone_group_id: int): + db_milestone = Milestone(group_id=milestone_group_id) + add(session, db_milestone) + for language in session.exec(select(Language)).all(): + session.add( + MilestoneText(milestone_id=db_milestone.id, lang_id=language.id) + ) + session.commit() + session.refresh(db_milestone) + return db_milestone + + @router.put("/milestones/", response_model=MilestoneAdmin) + def update_milestone( + session: SessionDep, + milestone: MilestoneAdmin, + ): + db_milestone = get(session, Milestone, milestone.id) + for key, value in milestone.model_dump(exclude={"text", "images"}).items(): + setattr(db_milestone, key, value) + update_milestone_text(session, milestone) + add(session, db_milestone) + return db_milestone + + @router.delete("/milestones/{milestone_id}") + def delete_milestone(session: SessionDep, milestone_id: int): + milestone = get(session, Milestone, milestone_id) + session.delete(milestone) + session.commit() + return {"ok": True} + + @router.post("/milestone-images/{milestone_id}", response_model=MilestoneImage) + async def upload_milestone_image( + session: SessionDep, milestone_id: int, file: UploadFile + ): + milestone = get(session, Milestone, milestone_id) + milestone_image = MilestoneImage(milestone_id=milestone.id, approved=True) + add(session, milestone_image) + filename = f"m{milestone_image.id}.jpg" + milestone_image.filename = filename + write_file(file, f"{app_settings.STATIC_FILES_PATH}/{filename}") + session.commit() + session.refresh(milestone_image) + return milestone_image + + @router.delete("/milestone-images/{milestone_image_id}") + async def delete_milestone_image(session: SessionDep, milestone_image_id: int): + milestone_image = get(session, MilestoneImage, milestone_image_id) + pathlib.Path( + f"{app_settings.STATIC_FILES_PATH}/m{milestone_image.id}.jpg" + ).unlink(missing_ok=True) + session.delete(milestone_image) + session.commit() + return {"ok": True} + + @router.get("/milestone-age-scores/{milestone_id}") + def get_milestone_age_scores( + session: SessionDep, milestone_id: int + ) -> MilestoneAgeScores: + return calculate_milestone_age_scores(session, milestone_id) + + return router diff --git a/mondey_backend/src/mondey_backend/routers/admin_routers/questions.py b/mondey_backend/src/mondey_backend/routers/admin_routers/questions.py new file mode 100644 index 00000000..7e295d11 --- /dev/null +++ b/mondey_backend/src/mondey_backend/routers/admin_routers/questions.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from fastapi import APIRouter +from sqlmodel import col +from sqlmodel import select + +from ...dependencies import SessionDep +from ...models.milestones import Language +from ...models.questions import ChildQuestion +from ...models.questions import ChildQuestionAdmin +from ...models.questions import ChildQuestionText +from ...models.questions import UserQuestion +from ...models.questions import UserQuestionAdmin +from ...models.questions import UserQuestionText +from ..utils import add +from ..utils import get +from ..utils import update_child_question_text +from ..utils import update_user_question_text + + +def create_router() -> APIRouter: + router = APIRouter() + + # User question CRUD endpoints + @router.get("/user-questions/", response_model=list[UserQuestionAdmin]) + def get_user_questions_admin(session: SessionDep): + user_questions = session.exec( + select(UserQuestion).order_by(col(UserQuestion.order)) + ).all() + return user_questions + + @router.post("/user-questions/", response_model=UserQuestionAdmin) + def create_user_question(session: SessionDep): + user_question = UserQuestion() + add(session, user_question) + for language in session.exec(select(Language)).all(): + session.add( + UserQuestionText(user_question_id=user_question.id, lang_id=language.id) + ) + session.commit() + session.refresh(user_question) + return user_question + + @router.put("/user-questions/", response_model=UserQuestionAdmin) + def update_user_question( + session: SessionDep, + user_question: UserQuestionAdmin, + ): + db_user_question = get(session, UserQuestion, user_question.id) + for key, value in user_question.model_dump(exclude={"text"}).items(): + setattr(db_user_question, key, value) + update_user_question_text(session, user_question) + add(session, db_user_question) + return db_user_question + + @router.delete("/user-questions/{user_question_id}") + def delete_user_question(session: SessionDep, user_question_id: int): + question = get(session, UserQuestion, user_question_id) + session.delete(question) + session.commit() + return {"ok": True} + + # Child question CRUD endpoints + @router.get("/child-questions/", response_model=list[ChildQuestionAdmin]) + def get_child_questions_admin(session: SessionDep): + user_questions = session.exec( + select(ChildQuestion).order_by(col(ChildQuestion.order)) + ).all() + return user_questions + + @router.post("/child-questions/", response_model=ChildQuestionAdmin) + def create_child_question( + session: SessionDep, + ): + child_question = ChildQuestion() + add(session, child_question) + for language in session.exec(select(Language)).all(): + session.add( + ChildQuestionText( + child_question_id=child_question.id, lang_id=language.id + ) + ) + session.commit() + session.refresh(child_question) + return child_question + + @router.put("/child-questions/", response_model=ChildQuestionAdmin) + def update_child_question( + session: SessionDep, + child_question: ChildQuestionAdmin, + ): + db_child_question = get(session, ChildQuestion, child_question.id) + + for key, value in child_question.model_dump(exclude={"text"}).items(): + setattr(db_child_question, key, value) + update_child_question_text(session, child_question) + add(session, db_child_question) + return db_child_question + + @router.delete("/child-questions/{child_question_id}") + def delete_child_question(session: SessionDep, child_question_id: int): + question = get(session, ChildQuestion, child_question_id) + session.delete(question) + session.commit() + return {"ok": True} + + return router diff --git a/mondey_backend/src/mondey_backend/routers/admin_routers/users.py b/mondey_backend/src/mondey_backend/routers/admin_routers/users.py new file mode 100644 index 00000000..5522c51c --- /dev/null +++ b/mondey_backend/src/mondey_backend/routers/admin_routers/users.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from fastapi import APIRouter +from sqlmodel import select + +from ...dependencies import UserAsyncSessionDep +from ...models.users import User +from ...models.users import UserRead + + +def create_router() -> APIRouter: + router = APIRouter() + + @router.get("/users/", response_model=list[UserRead]) + async def get_users(session: UserAsyncSessionDep): + users = await session.execute(select(User)) + return users.scalars().all() + + return router diff --git a/mondey_backend/src/mondey_backend/routers/users.py b/mondey_backend/src/mondey_backend/routers/users.py index 70a7a552..e5453d0e 100644 --- a/mondey_backend/src/mondey_backend/routers/users.py +++ b/mondey_backend/src/mondey_backend/routers/users.py @@ -100,7 +100,9 @@ async def get_child_image( f"{app_settings.PRIVATE_FILES_PATH}/children/{child.id}.jpg" ) if not image_path.exists(): - image_path = pathlib.Path(f"{app_settings.DATA_FILES_PATH}/dummy.jpg") + image_path = pathlib.Path( + f"{app_settings.STATIC_FILES_PATH}/default_child.jpg" + ) return image_path @router.put("/children-images/{child_id}") diff --git a/mondey_backend/src/mondey_backend/settings.py b/mondey_backend/src/mondey_backend/settings.py index ff8f78ea..af2a7acd 100644 --- a/mondey_backend/src/mondey_backend/settings.py +++ b/mondey_backend/src/mondey_backend/settings.py @@ -9,7 +9,6 @@ class AppSettings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") # these defaults are for local development and are used if the environment variables are not set SECRET: str = "abc123" - DATA_FILES_PATH: str = "data" DATABASE_PATH: str = "db" STATIC_FILES_PATH: str = "static" PRIVATE_FILES_PATH: str = "private" diff --git a/mondey_backend/tests/conftest.py b/mondey_backend/tests/conftest.py index 919c8880..43a9ece6 100644 --- a/mondey_backend/tests/conftest.py +++ b/mondey_backend/tests/conftest.py @@ -2,17 +2,21 @@ import datetime import pathlib -import shutil import pytest +import pytest_asyncio from fastapi import FastAPI from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.asyncio import async_sessionmaker +from sqlalchemy.ext.asyncio import create_async_engine from sqlmodel import Session from sqlmodel import SQLModel from sqlmodel import create_engine from sqlmodel.pool import StaticPool from mondey_backend import settings +from mondey_backend.databases.users import get_async_session from mondey_backend.dependencies import current_active_researcher from mondey_backend.dependencies import current_active_superuser from mondey_backend.dependencies import current_active_user @@ -33,23 +37,23 @@ from mondey_backend.models.questions import UserAnswer from mondey_backend.models.questions import UserQuestion from mondey_backend.models.questions import UserQuestionText +from mondey_backend.models.users import Base +from mondey_backend.models.users import User from mondey_backend.models.users import UserRead -@pytest.fixture(scope="session") +@pytest.fixture() def static_dir(tmp_path_factory: pytest.TempPathFactory): - # use the same single temporary directory in all tests for static files static_dir = tmp_path_factory.mktemp("static") - # add some milestone image files - for filename in ["m1.jpg", "m2.jpg", "m3.jpg"]: + # add some milestone image files & default child image + for filename in ["m1.jpg", "m2.jpg", "m3.jpg", "default_child.jpg"]: with (static_dir / filename).open("w") as f: f.write(filename) return static_dir -@pytest.fixture(scope="session") +@pytest.fixture() def private_dir(tmp_path_factory: pytest.TempPathFactory): - # use the same single temporary directory in all tests for private files private_dir = tmp_path_factory.mktemp("private") children_dir = private_dir / "children" children_dir.mkdir() @@ -60,23 +64,13 @@ def private_dir(tmp_path_factory: pytest.TempPathFactory): return private_dir -@pytest.fixture(scope="session") -def data_dir(tmp_path_factory: pytest.TempPathFactory): - # use the same single temporary directory in all tests for private files - data_dir = tmp_path_factory.mktemp("data") - original_dummy_path = pathlib.Path(__file__).parent.parent / "data" / "dummy.jpg" - data_dir.mkdir(exist_ok=True) - shutil.copy(original_dummy_path, data_dir / "dummy.jpg") - return data_dir - - @pytest.fixture def children(): today = datetime.datetime.today() nine_months_ago = today - datetime.timedelta(days=9 * 30) twenty_months_ago = today - datetime.timedelta(days=20 * 30) return [ - # ~9month old child for user 1 + # ~9month old child for user (id 3) { "id": 1, "name": "child1", @@ -84,7 +78,7 @@ def children(): "birth_month": nine_months_ago.month, "has_image": False, }, - # ~20month old child for user 1 + # ~20month old child for user (id 3) { "id": 2, "name": "child2", @@ -92,7 +86,7 @@ def children(): "birth_month": twenty_months_ago.month, "has_image": True, }, - # ~5 year old child for user 3 + # ~5 year old child for admin user (id 1) { "birth_month": 7, "birth_year": today.year - 5, @@ -103,6 +97,32 @@ def children(): ] +@pytest_asyncio.fixture(loop_scope="function") +async def user_session( + active_admin_user: UserRead, + active_research_user: UserRead, + active_user: UserRead, + active_user2: UserRead, +): + # use a new in-memory SQLite user database for each test + engine = create_async_engine("sqlite+aiosqlite://") + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + async_session_maker = async_sessionmaker(engine, expire_on_commit=False) + async with async_session_maker() as session: + for user_read in [ + active_admin_user, + active_research_user, + active_user, + active_user2, + ]: + user = User(hashed_password="abc") + for k, v in user_read.model_dump().items(): + setattr(user, k, v) + session.add(user) + yield session + + @pytest.fixture def session(children: list[dict]): # use a new in-memory SQLite database for each test @@ -180,15 +200,15 @@ def session(children: list[dict]): session.add(MilestoneImage(milestone_id=1, filename="m2.jpg", approved=True)) session.add(MilestoneImage(milestone_id=2, filename="m3.jpg", approved=True)) session.commit() - for child, user_id in zip(children, [1, 1, 3], strict=False): + for child, user_id in zip(children, [3, 3, 1], strict=False): session.add(Child.model_validate(child, update={"user_id": user_id})) today = datetime.datetime.today() last_month = today - datetime.timedelta(days=30) - # add an (expired) milestone answer session for child 1 / user 1 with 2 answers + # add an (expired) milestone answer session for child 1 / user (id 3) with 2 answers session.add( MilestoneAnswerSession( child_id=1, - user_id=1, + user_id=3, created_at=datetime.datetime( last_month.year, last_month.month, last_month.day ), @@ -196,16 +216,16 @@ def session(children: list[dict]): ) session.add(MilestoneAnswer(answer_session_id=1, milestone_id=1, answer=1)) session.add(MilestoneAnswer(answer_session_id=1, milestone_id=2, answer=0)) - # add another (current) milestone answer session for child 1 / user 1 with 2 answers to the same questions - session.add(MilestoneAnswerSession(child_id=1, user_id=1, created_at=today)) + # add another (current) milestone answer session for child 1 / user (id 3) with 2 answers to the same questions + session.add(MilestoneAnswerSession(child_id=1, user_id=3, created_at=today)) # add two milestone answers session.add(MilestoneAnswer(answer_session_id=2, milestone_id=1, answer=3)) session.add(MilestoneAnswer(answer_session_id=2, milestone_id=2, answer=2)) - # add an (expired) milestone answer session for child 3 / user 3 with 1 answer + # add an (expired) milestone answer session for child 3 / admin user (id 1) with 1 answer session.add( MilestoneAnswerSession( child_id=3, - user_id=3, + user_id=1, created_at=datetime.datetime(today.year - 1, 1, 1), ) ) @@ -340,12 +360,12 @@ def session(children: list[dict]): for lang in child_question.text: session.add(child_question.text[lang]) - # add user answers for user 1 + # add user answers for user (id 3) session.add( UserAnswer( id=1, question_id=1, - user_id=1, + user_id=3, answer="lorem ipsum", additional_answer=None, ) @@ -354,18 +374,18 @@ def session(children: list[dict]): UserAnswer( id=2, question_id=2, - user_id=1, + user_id=3, answer="other", additional_answer="dolor sit", ) ) - # add child answers for user 1 + # add child answers for user (id 3) session.add( ChildAnswer( id=1, question_id=1, - user_id=1, + user_id=3, child_id=1, answer="a", additional_answer=None, @@ -375,7 +395,7 @@ def session(children: list[dict]): ChildAnswer( id=2, question_id=2, - user_id=1, + user_id=3, child_id=1, answer="other", additional_answer="dolor sit", @@ -581,7 +601,7 @@ def default_user_question_admin(): @pytest.fixture def active_admin_user(): return UserRead( - id=3, + id=1, email="admin@mondey.de", is_active=True, is_superuser=True, @@ -605,7 +625,7 @@ def active_research_user(): @pytest.fixture def active_user(): return UserRead( - id=1, + id=3, email="user@mondey.de", is_active=True, is_superuser=False, @@ -615,9 +635,9 @@ def active_user(): @pytest.fixture -def second_active_user(): +def active_user2(): return UserRead( - id=2, + id=4, email="user2@mondey.de", is_active=True, is_superuser=False, @@ -626,26 +646,30 @@ def second_active_user(): ) -@pytest.fixture(scope="session") -def app(static_dir: pathlib.Path, private_dir: pathlib.Path, data_dir: pathlib.Path): +@pytest.fixture(scope="function") +def app( + static_dir: pathlib.Path, + private_dir: pathlib.Path, + session: Session, + user_session: AsyncSession, +): settings.app_settings.STATIC_FILES_PATH = str(static_dir) settings.app_settings.PRIVATE_FILES_PATH = str(private_dir) - settings.app_settings.DATA_FILES_PATH = str(data_dir) app = create_app() - return app + app.dependency_overrides[get_session] = lambda: session + app.dependency_overrides[get_async_session] = lambda: user_session + yield app @pytest.fixture -def public_client(app: FastAPI, session: Session): - app.dependency_overrides[get_session] = lambda: session +def public_client(app: FastAPI): client = TestClient(app) yield client app.dependency_overrides.clear() @pytest.fixture -def user_client(app: FastAPI, session: Session, active_user: UserRead): - app.dependency_overrides[get_session] = lambda: session +def user_client(app: FastAPI, active_user: UserRead): app.dependency_overrides[current_active_user] = lambda: active_user client = TestClient(app) yield client @@ -653,9 +677,11 @@ def user_client(app: FastAPI, session: Session, active_user: UserRead): @pytest.fixture -def second_user_client(app: FastAPI, session: Session, second_active_user: UserRead): - app.dependency_overrides[get_session] = lambda: session - app.dependency_overrides[current_active_user] = lambda: second_active_user +def user_client2( + app: FastAPI, + active_user2: UserRead, +): + app.dependency_overrides[current_active_user] = lambda: active_user2 client = TestClient(app) yield client app.dependency_overrides.clear() @@ -664,10 +690,8 @@ def second_user_client(app: FastAPI, session: Session, second_active_user: UserR @pytest.fixture def research_client( app: FastAPI, - session: Session, active_research_user: UserRead, ): - app.dependency_overrides[get_session] = lambda: session app.dependency_overrides[current_active_user] = lambda: active_research_user app.dependency_overrides[current_active_researcher] = lambda: active_research_user client = TestClient(app) @@ -679,9 +703,9 @@ def research_client( def admin_client( app: FastAPI, session: Session, + user_session: AsyncSession, active_admin_user: UserRead, ): - app.dependency_overrides[get_session] = lambda: session app.dependency_overrides[current_active_user] = lambda: active_admin_user app.dependency_overrides[current_active_superuser] = lambda: active_admin_user client = TestClient(app) diff --git a/mondey_backend/tests/routers/admin_routers/test_admin_languages.py b/mondey_backend/tests/routers/admin_routers/test_admin_languages.py new file mode 100644 index 00000000..ea2c6a61 --- /dev/null +++ b/mondey_backend/tests/routers/admin_routers/test_admin_languages.py @@ -0,0 +1,64 @@ +import json +import pathlib + +import pytest +from fastapi.testclient import TestClient + + +def test_post_language(admin_client: TestClient): + response = admin_client.post("/admin/languages/", json={"id": "es"}) + assert response.status_code == 200 + assert response.json() == {"id": "es"} + + +def test_post_language_invalid_already_exists(admin_client: TestClient): + response = admin_client.post("/admin/languages/", json={"id": "de"}) + assert response.status_code == 400 + + +def test_delete_language(admin_client: TestClient): + response = admin_client.delete("/admin/languages/fr") + assert response.status_code == 200 + assert admin_client.get("/languages").json() == ["de", "en"] + + +@pytest.mark.parametrize("lang_id", ["de", "en"]) +def test_delete_language_invalid_de_en_cannot_be_deleted( + admin_client: TestClient, lang_id: str +): + response = admin_client.delete(f"/admin/languages/{lang_id}") + assert response.status_code == 400 + + +def test_delete_language_invalid_language_id(admin_client: TestClient): + response = admin_client.delete("/admin/languages/zz") + assert response.status_code == 404 + + +def test_update_i18n(admin_client: TestClient, static_dir: pathlib.Path): + assert admin_client.post("/admin/languages/", json={"id": "nl"}).status_code == 200 + i18_json_path = static_dir / "i18n" / "nl.json" + i18_json = { + "s1": {"k1": "v1", "k2": "v2"}, + "accents": {"k1": "v1", "äéœ": "óíüúëþ"}, + } + assert not i18_json_path.is_file() + response = admin_client.put("/admin/i18n/nl", json=i18_json) + assert response.status_code == 200 + assert i18_json_path.is_file() + with open(i18_json_path) as f: + assert json.load(f) == i18_json + i18_json["s1"]["k1"] = "MODIFIED!" + response = admin_client.put("/admin/i18n/nl", json=i18_json) + assert response.status_code == 200 + with open(i18_json_path) as f: + assert json.load(f) == i18_json + + +def test_update_i18n_invalid_json(admin_client: TestClient, static_dir: pathlib.Path): + i18_json = { + "valid-section": {"key1": "value1"}, + "invalid-section": "this-value-should-be-a-dict!", + } + response = admin_client.put("/admin/i18n/en", json=i18_json) + assert response.status_code == 422 diff --git a/mondey_backend/tests/routers/admin_routers/test_admin_milestones.py b/mondey_backend/tests/routers/admin_routers/test_admin_milestones.py new file mode 100644 index 00000000..cfe30535 --- /dev/null +++ b/mondey_backend/tests/routers/admin_routers/test_admin_milestones.py @@ -0,0 +1,220 @@ +import pathlib + +import pytest +from fastapi.testclient import TestClient + + +def test_get_milestone_groups( + admin_client: TestClient, milestone_group_admin1: dict, milestone_group_admin2: dict +): + response = admin_client.get("/admin/milestone-groups/") + assert response.status_code == 200 + assert len(response.json()) == 2 + assert response.json() == [milestone_group_admin2, milestone_group_admin1] + + +def test_post_milestone_group(admin_client: TestClient): + response = admin_client.post("/admin/milestone-groups/") + assert response.status_code == 200 + assert response.json() == { + "id": 3, + "order": 0, + "text": { + "de": { + "group_id": 3, + "lang_id": "de", + "title": "", + "desc": "", + }, + "en": { + "group_id": 3, + "lang_id": "en", + "title": "", + "desc": "", + }, + "fr": { + "group_id": 3, + "lang_id": "fr", + "title": "", + "desc": "", + }, + }, + "milestones": [], + } + + +def test_put_milestone_group(admin_client: TestClient, milestone_group_admin1: dict): + milestone_group = milestone_group_admin1 + milestone_group["order"] = 6 + milestone_group["text"]["de"]["title"] = "asdsd" + milestone_group["text"]["de"]["desc"] = "12xzascdasdf" + milestone_group["text"]["en"]["title"] = "asqwdreqweqw" + milestone_group["text"]["en"]["desc"] = "th567" + response = admin_client.put("/admin/milestone-groups", json=milestone_group) + assert response.status_code == 200 + assert response.json() == milestone_group + + +def test_delete_milestone_group(admin_client: TestClient): + response = admin_client.delete("/admin/milestone-groups/2") + assert response.status_code == 200 + response = admin_client.delete("/admin/milestone-groups/2") + assert response.status_code == 404 + + +def test_delete_milestone_group_invalid_group_id(admin_client: TestClient): + response = admin_client.delete("/admin/milestone-groups/692") + assert response.status_code == 404 + + +@pytest.mark.parametrize( + "endpoint", + ["/admin/languages/fr", "/admin/milestone-groups/1", "/admin/milestones/1"], +) +@pytest.mark.parametrize( + "client_type", ["public_client", "user_client", "research_client"] +) +def test_delete_endpoints_invalid_admin_user( + endpoint: str, client_type: str, request: pytest.FixtureRequest +): + client = request.getfixturevalue(client_type) + response = client.delete(endpoint) + assert response.status_code == 401 + + +def test_put_milestone_group_image( + admin_client: TestClient, static_dir: pathlib.Path, jpg_file: pathlib.Path +): + static_dir_jpg = static_dir / "mg1.jpg" + assert not static_dir_jpg.is_file() + with open(jpg_file, "rb") as f: + response = admin_client.put( + "/admin/milestone-group-images/1", + files={"file": ("filename", f, "image/jpeg")}, + ) + assert response.status_code == 200 + assert static_dir_jpg.is_file() + + +def test_post_milestone(admin_client: TestClient): + response = admin_client.post("/admin/milestones/2") + assert response.status_code == 200 + assert response.json() == { + "id": 6, + "group_id": 2, + "order": 0, + "expected_age_months": 12, + "text": { + "de": { + "milestone_id": 6, + "lang_id": "de", + "title": "", + "desc": "", + "obs": "", + "help": "", + }, + "en": { + "milestone_id": 6, + "lang_id": "en", + "title": "", + "desc": "", + "obs": "", + "help": "", + }, + "fr": { + "milestone_id": 6, + "lang_id": "fr", + "title": "", + "desc": "", + "obs": "", + "help": "", + }, + }, + "images": [], + } + + +def test_put_milestone(admin_client: TestClient, milestone_group_admin1: dict): + milestone = milestone_group_admin1["milestones"][0] + milestone["order"] = 6 + milestone["expected_age_months"] = 11 + milestone["text"]["de"]["title"] = "asdsd" + milestone["text"]["de"]["desc"] = "12xzascdasdf" + milestone["text"]["de"]["obs"] = "asdrgf" + milestone["text"]["de"]["help"] = "jgfhj" + milestone["text"]["en"]["title"] = "asqwdreqweqw" + milestone["text"]["en"]["desc"] = "th567" + response = admin_client.put("/admin/milestones", json=milestone) + assert response.status_code == 200 + assert response.json() == milestone + + +def test_delete_milestone(admin_client: TestClient): + assert admin_client.get("/milestones/2").status_code == 200 + response = admin_client.delete("/admin/milestones/2") + assert response.status_code == 200 + assert admin_client.get("/milestones/2").status_code == 404 + response = admin_client.delete("/admin/milestones/2") + assert response.status_code == 404 + + +def test_post_milestone_image( + admin_client: TestClient, static_dir: pathlib.Path, jpg_file: pathlib.Path +): + # 3 milestone images already exist + assert len(admin_client.get("/milestones/1").json()["images"]) == 2 + assert len(admin_client.get("/milestones/2").json()["images"]) == 1 + assert len(admin_client.get("/milestones/3").json()["images"]) == 0 + assert len(admin_client.get("/milestones/4").json()["images"]) == 0 + assert len(admin_client.get("/milestones/5").json()["images"]) == 0 + # image ids are sequential + milestone_image_id = 3 + # add an image to each milestone + for milestone_id in [1, 2, 3, 4, 5]: + milestone_image_id += 1 + filename = f"m{milestone_image_id}.jpg" + static_dir_jpg = static_dir / filename + assert not static_dir_jpg.is_file() + with open(jpg_file, "rb") as f: + response = admin_client.post( + f"/admin/milestone-images/{milestone_id}", + files={"file": ("filename", f, "image/jpeg")}, + ) + assert response.status_code == 200 + assert response.json()["filename"] == filename + assert static_dir_jpg.is_file() + assert len(admin_client.get("/milestones/1").json()["images"]) == 3 + assert len(admin_client.get("/milestones/2").json()["images"]) == 2 + assert len(admin_client.get("/milestones/3").json()["images"]) == 1 + assert len(admin_client.get("/milestones/4").json()["images"]) == 1 + assert len(admin_client.get("/milestones/5").json()["images"]) == 1 + # remove added images + for milestone_image_id in range(4, 9): + filename = f"m{milestone_image_id}.jpg" + static_dir_jpg = static_dir / filename + assert static_dir_jpg.is_file() + assert ( + admin_client.delete( + f"/admin/milestone-images/{milestone_image_id}" + ).status_code + == 200 + ) + assert not static_dir_jpg.is_file() + assert len(admin_client.get("/milestones/1").json()["images"]) == 2 + assert len(admin_client.get("/milestones/2").json()["images"]) == 1 + assert len(admin_client.get("/milestones/3").json()["images"]) == 0 + assert len(admin_client.get("/milestones/4").json()["images"]) == 0 + assert len(admin_client.get("/milestones/5").json()["images"]) == 0 + + +def test_get_milestone_age_scores(admin_client: TestClient): + response = admin_client.get("/admin/milestone-age-scores/1") + assert response.status_code == 200 + # child 1 scored + # - 2 @ ~8 months old + # - 4 @ ~9 months old + assert response.json()["expected_age"] == 9 + assert response.json()["scores"][7]["avg_score"] == pytest.approx(0.0) + assert response.json()["scores"][8]["avg_score"] == pytest.approx(2.0) + assert response.json()["scores"][9]["avg_score"] == pytest.approx(4.0) + assert response.json()["scores"][10]["avg_score"] == pytest.approx(0.0) diff --git a/mondey_backend/tests/routers/admin_routers/test_admin_questions.py b/mondey_backend/tests/routers/admin_routers/test_admin_questions.py new file mode 100644 index 00000000..3a265b6b --- /dev/null +++ b/mondey_backend/tests/routers/admin_routers/test_admin_questions.py @@ -0,0 +1,249 @@ +from fastapi.testclient import TestClient +from sqlmodel import select + +from mondey_backend.models.questions import ChildQuestion +from mondey_backend.models.questions import UserQuestion + + +def test_get_user_question_admin_works(admin_client: TestClient, user_questions): + response = admin_client.get("/admin/user-questions/") + + assert response.status_code == 200 + + assert [element["order"] for element in response.json()] == [1, 2] + assert response.json() == user_questions + + +def test_create_user_question_works(admin_client: TestClient): + response = admin_client.post("/admin/user-questions/") + assert response.status_code == 200 + assert response.json() == { + "id": 3, + "order": 0, + "component": "select", + "type": "text", + "options": "", + "text": { + "de": { + "options_json": "", + "user_question_id": 3, + "options": "", + "lang_id": "de", + "question": "", + }, + "en": { + "options_json": "", + "user_question_id": 3, + "options": "", + "lang_id": "en", + "question": "", + }, + "fr": { + "options_json": "", + "user_question_id": 3, + "options": "", + "lang_id": "fr", + "question": "", + }, + }, + "additional_option": "", + } + + +def test_update_user_question_works( + admin_client: TestClient, default_user_question_admin +): + response = admin_client.put( + "/admin/user-questions/", json=default_user_question_admin + ) + + assert response.status_code == 200 + + assert response.json() == default_user_question_admin + + +def test_update_user_question_id_not_there(admin_client: TestClient): + user_question_admin = { + "id": 5, + "component": "textarea", + "type": "other_thing", + "order": 0, + "options": "some_options", + "text": { + "de": { + "options_json": "", + "user_question_id": 5, + "options": "", + "lang_id": "de", + "question": "", + }, + "en": { + "options_json": "", + "user_question_id": 5, + "options": "", + "lang_id": "en", + "question": "", + }, + "fr": { + "options_json": "", + "user_question_id": 5, + "options": "", + "lang_id": "fr", + "question": "", + }, + }, + "additional_option": "nothing", + } + + response = admin_client.put("/admin/user-questions/", json=user_question_admin) + + assert response.status_code == 404 + + +def test_delete_user_question_works(session, admin_client: TestClient): + response = admin_client.delete("/admin/user-questions/1") + + assert response.status_code == 200 + assert response.json() == {"ok": True} + + user_questions = session.exec(select(UserQuestion)).all() + assert len(user_questions) == 1 + assert user_questions[0].id == 2 + + +def test_delete_user_question_id_not_there(admin_client: TestClient): + response = admin_client.delete("/admin/user-questions/12") + + assert response.status_code == 404 + + +def test_get_child_question_admin_works(admin_client: TestClient, child_questions): + response = admin_client.get("/admin/child-questions/") + + assert response.status_code == 200 + assert [element["order"] for element in response.json()] == [0, 1] + assert response.json() == child_questions + + +def test_create_child_question_works(admin_client: TestClient): + response = admin_client.post("/admin/child-questions/") + + assert response.status_code == 200 + assert response.json() == { + "id": 3, + "order": 0, + "component": "select", + "type": "text", + "options": "", + "text": { + "de": { + "options_json": "", + "child_question_id": 3, + "options": "", + "lang_id": "de", + "question": "", + }, + "en": { + "options_json": "", + "child_question_id": 3, + "options": "", + "lang_id": "en", + "question": "", + }, + "fr": { + "options_json": "", + "child_question_id": 3, + "options": "", + "lang_id": "fr", + "question": "", + }, + }, + "additional_option": "", + } + + +def test_update_child_question_works(admin_client: TestClient): + child_question_admin = { + "id": 2, + "component": "textarea", + "type": "other_thing", + "order": 0, + "options": "some_options", + "text": { + "de": { + "options_json": "", + "child_question_id": 2, + "options": "", + "lang_id": "de", + "question": "", + }, + "en": { + "options_json": "", + "child_question_id": 2, + "options": "", + "lang_id": "en", + "question": "", + }, + "fr": { + "options_json": "", + "child_question_id": 2, + "options": "", + "lang_id": "fr", + "question": "", + }, + }, + "additional_option": "nothing", + } + response = admin_client.put("/admin/child-questions", json=child_question_admin) + assert response.status_code == 200 + + +def test_update_child_question_id_not_there(admin_client: TestClient): + child_question_admin = { + "id": 5, + "component": "textarea", + "type": "other_thing", + "order": 0, + "options": "some_options", + "text": { + "de": { + "options_json": "", + "child_question_id": 5, + "options": "", + "lang_id": "de", + "question": "", + }, + "en": { + "options_json": "", + "child_question_id": 5, + "options": "", + "lang_id": "en", + "question": "", + }, + "fr": { + "options_json": "", + "child_question_id": 5, + "options": "", + "lang_id": "fr", + "question": "", + }, + }, + "additional_option": "nothing", + } + response = admin_client.put("/admin/child-questions/", json=child_question_admin) + assert response.status_code == 404 + + +def test_delete_child_question_works(session, admin_client: TestClient): + response = admin_client.delete("/admin/child-questions/1") + assert response.status_code == 200 + assert response.json() == {"ok": True} + + child_questions = session.exec(select(ChildQuestion)).all() + assert len(child_questions) == 1 + assert child_questions[0].id == 2 + + +def test_delete_child_question_id_not_there(admin_client: TestClient): + response = admin_client.delete("/admin/child-questions/12") + assert response.status_code == 404 diff --git a/mondey_backend/tests/routers/admin_routers/test_admin_users.py b/mondey_backend/tests/routers/admin_routers/test_admin_users.py new file mode 100644 index 00000000..55964dce --- /dev/null +++ b/mondey_backend/tests/routers/admin_routers/test_admin_users.py @@ -0,0 +1,20 @@ +from fastapi.testclient import TestClient + + +def test_users(admin_client: TestClient): + response = admin_client.get("/admin/users/") + assert response.status_code == 200 + users = response.json() + assert len(users) == 4 + assert users[0]["id"] == 1 + assert not users[0]["is_researcher"] + assert users[0]["is_superuser"] + assert users[1]["id"] == 2 + assert users[1]["is_researcher"] + assert not users[1]["is_superuser"] + assert users[2]["id"] == 3 + assert not users[2]["is_researcher"] + assert not users[2]["is_superuser"] + assert users[3]["id"] == 4 + assert not users[3]["is_researcher"] + assert not users[3]["is_superuser"] diff --git a/mondey_backend/tests/routers/test_admin.py b/mondey_backend/tests/routers/test_admin.py deleted file mode 100644 index 6f9804c9..00000000 --- a/mondey_backend/tests/routers/test_admin.py +++ /dev/null @@ -1,531 +0,0 @@ -import json -import pathlib - -import pytest -from fastapi.testclient import TestClient -from sqlmodel import select - -from mondey_backend.models.questions import ChildQuestion -from mondey_backend.models.questions import UserQuestion - - -def test_post_language(admin_client: TestClient): - response = admin_client.post("/admin/languages/", json={"id": "es"}) - assert response.status_code == 200 - assert response.json() == {"id": "es"} - - -def test_post_language_invalid_already_exists(admin_client: TestClient): - response = admin_client.post("/admin/languages/", json={"id": "de"}) - assert response.status_code == 400 - - -def test_delete_language(admin_client: TestClient): - response = admin_client.delete("/admin/languages/fr") - assert response.status_code == 200 - assert admin_client.get("/languages").json() == ["de", "en"] - - -@pytest.mark.parametrize("lang_id", ["de", "en"]) -def test_delete_language_invalid_de_en_cannot_be_deleted( - admin_client: TestClient, lang_id: str -): - response = admin_client.delete(f"/admin/languages/{lang_id}") - assert response.status_code == 400 - - -def test_delete_language_invalid_language_id(admin_client: TestClient): - response = admin_client.delete("/admin/languages/zz") - assert response.status_code == 404 - - -def test_update_i18n(admin_client: TestClient, static_dir: pathlib.Path): - assert admin_client.post("/admin/languages/", json={"id": "nl"}).status_code == 200 - i18_json_path = static_dir / "i18n" / "nl.json" - i18_json = { - "s1": {"k1": "v1", "k2": "v2"}, - "accents": {"k1": "v1", "äéœ": "óíüúëþ"}, - } - assert not i18_json_path.is_file() - response = admin_client.put("/admin/i18n/nl", json=i18_json) - assert response.status_code == 200 - assert i18_json_path.is_file() - with open(i18_json_path) as f: - assert json.load(f) == i18_json - i18_json["s1"]["k1"] = "MODIFIED!" - response = admin_client.put("/admin/i18n/nl", json=i18_json) - assert response.status_code == 200 - with open(i18_json_path) as f: - assert json.load(f) == i18_json - - -def test_update_i18n_invalid_json(admin_client: TestClient, static_dir: pathlib.Path): - i18_json = { - "valid-section": {"key1": "value1"}, - "invalid-section": "this-value-should-be-a-dict!", - } - response = admin_client.put("/admin/i18n/en", json=i18_json) - assert response.status_code == 422 - - -def test_get_milestone_groups( - admin_client: TestClient, milestone_group_admin1: dict, milestone_group_admin2: dict -): - response = admin_client.get("/admin/milestone-groups/") - assert response.status_code == 200 - assert len(response.json()) == 2 - assert response.json() == [milestone_group_admin2, milestone_group_admin1] - - -def test_post_milestone_group(admin_client: TestClient): - response = admin_client.post("/admin/milestone-groups/") - assert response.status_code == 200 - assert response.json() == { - "id": 3, - "order": 0, - "text": { - "de": { - "group_id": 3, - "lang_id": "de", - "title": "", - "desc": "", - }, - "en": { - "group_id": 3, - "lang_id": "en", - "title": "", - "desc": "", - }, - "fr": { - "group_id": 3, - "lang_id": "fr", - "title": "", - "desc": "", - }, - }, - "milestones": [], - } - - -def test_put_milestone_group(admin_client: TestClient, milestone_group_admin1: dict): - milestone_group = milestone_group_admin1 - milestone_group["order"] = 6 - milestone_group["text"]["de"]["title"] = "asdsd" - milestone_group["text"]["de"]["desc"] = "12xzascdasdf" - milestone_group["text"]["en"]["title"] = "asqwdreqweqw" - milestone_group["text"]["en"]["desc"] = "th567" - response = admin_client.put("/admin/milestone-groups", json=milestone_group) - assert response.status_code == 200 - assert response.json() == milestone_group - - -def test_delete_milestone_group(admin_client: TestClient): - response = admin_client.delete("/admin/milestone-groups/2") - assert response.status_code == 200 - response = admin_client.delete("/admin/milestone-groups/2") - assert response.status_code == 404 - - -def test_delete_milestone_group_invalid_group_id(admin_client: TestClient): - response = admin_client.delete("/admin/milestone-groups/692") - assert response.status_code == 404 - - -@pytest.mark.parametrize( - "endpoint", - ["/admin/languages/fr", "/admin/milestone-groups/1", "/admin/milestones/1"], -) -@pytest.mark.parametrize( - "client_type", ["public_client", "user_client", "research_client"] -) -def test_delete_endpoints_invalid_admin_user( - endpoint: str, client_type: str, request: pytest.FixtureRequest -): - client = request.getfixturevalue(client_type) - response = client.delete(endpoint) - assert response.status_code == 401 - - -def test_put_milestone_group_image( - admin_client: TestClient, static_dir: pathlib.Path, jpg_file: pathlib.Path -): - static_dir_jpg = static_dir / "mg1.jpg" - assert not static_dir_jpg.is_file() - with open(jpg_file, "rb") as f: - response = admin_client.put( - "/admin/milestone-group-images/1", - files={"file": ("filename", f, "image/jpeg")}, - ) - assert response.status_code == 200 - assert static_dir_jpg.is_file() - - -def test_post_milestone(admin_client: TestClient): - response = admin_client.post("/admin/milestones/2") - assert response.status_code == 200 - assert response.json() == { - "id": 6, - "group_id": 2, - "order": 0, - "expected_age_months": 12, - "text": { - "de": { - "milestone_id": 6, - "lang_id": "de", - "title": "", - "desc": "", - "obs": "", - "help": "", - }, - "en": { - "milestone_id": 6, - "lang_id": "en", - "title": "", - "desc": "", - "obs": "", - "help": "", - }, - "fr": { - "milestone_id": 6, - "lang_id": "fr", - "title": "", - "desc": "", - "obs": "", - "help": "", - }, - }, - "images": [], - } - - -def test_put_milestone(admin_client: TestClient, milestone_group_admin1: dict): - milestone = milestone_group_admin1["milestones"][0] - milestone["order"] = 6 - milestone["expected_age_months"] = 11 - milestone["text"]["de"]["title"] = "asdsd" - milestone["text"]["de"]["desc"] = "12xzascdasdf" - milestone["text"]["de"]["obs"] = "asdrgf" - milestone["text"]["de"]["help"] = "jgfhj" - milestone["text"]["en"]["title"] = "asqwdreqweqw" - milestone["text"]["en"]["desc"] = "th567" - response = admin_client.put("/admin/milestones", json=milestone) - assert response.status_code == 200 - assert response.json() == milestone - - -def test_delete_milestone(admin_client: TestClient): - assert admin_client.get("/milestones/2").status_code == 200 - response = admin_client.delete("/admin/milestones/2") - assert response.status_code == 200 - assert admin_client.get("/milestones/2").status_code == 404 - response = admin_client.delete("/admin/milestones/2") - assert response.status_code == 404 - - -def test_post_milestone_image( - admin_client: TestClient, static_dir: pathlib.Path, jpg_file: pathlib.Path -): - # 3 milestone images already exist - assert len(admin_client.get("/milestones/1").json()["images"]) == 2 - assert len(admin_client.get("/milestones/2").json()["images"]) == 1 - assert len(admin_client.get("/milestones/3").json()["images"]) == 0 - assert len(admin_client.get("/milestones/4").json()["images"]) == 0 - assert len(admin_client.get("/milestones/5").json()["images"]) == 0 - # image ids are sequential - milestone_image_id = 3 - # add an image to each milestone - for milestone_id in [1, 2, 3, 4, 5]: - milestone_image_id += 1 - filename = f"m{milestone_image_id}.jpg" - static_dir_jpg = static_dir / filename - assert not static_dir_jpg.is_file() - with open(jpg_file, "rb") as f: - response = admin_client.post( - f"/admin/milestone-images/{milestone_id}", - files={"file": ("filename", f, "image/jpeg")}, - ) - assert response.status_code == 200 - assert response.json()["filename"] == filename - assert static_dir_jpg.is_file() - assert len(admin_client.get("/milestones/1").json()["images"]) == 3 - assert len(admin_client.get("/milestones/2").json()["images"]) == 2 - assert len(admin_client.get("/milestones/3").json()["images"]) == 1 - assert len(admin_client.get("/milestones/4").json()["images"]) == 1 - assert len(admin_client.get("/milestones/5").json()["images"]) == 1 - # remove added images - for milestone_image_id in range(4, 9): - filename = f"m{milestone_image_id}.jpg" - static_dir_jpg = static_dir / filename - assert static_dir_jpg.is_file() - assert ( - admin_client.delete( - f"/admin/milestone-images/{milestone_image_id}" - ).status_code - == 200 - ) - assert not static_dir_jpg.is_file() - assert len(admin_client.get("/milestones/1").json()["images"]) == 2 - assert len(admin_client.get("/milestones/2").json()["images"]) == 1 - assert len(admin_client.get("/milestones/3").json()["images"]) == 0 - assert len(admin_client.get("/milestones/4").json()["images"]) == 0 - assert len(admin_client.get("/milestones/5").json()["images"]) == 0 - - -def test_get_milestone_age_scores(admin_client: TestClient): - response = admin_client.get("/admin/milestone-age-scores/1") - assert response.status_code == 200 - # child 1 scored - # - 2 @ ~8 months old - # - 4 @ ~9 months old - assert response.json()["expected_age"] == 9 - assert response.json()["scores"][7]["avg_score"] == pytest.approx(0.0) - assert response.json()["scores"][8]["avg_score"] == pytest.approx(2.0) - assert response.json()["scores"][9]["avg_score"] == pytest.approx(4.0) - assert response.json()["scores"][10]["avg_score"] == pytest.approx(0.0) - - -# tests for user questions - - -def test_get_user_question_admin_works(admin_client: TestClient, user_questions): - response = admin_client.get("/admin/user-questions/") - - assert response.status_code == 200 - - assert [element["order"] for element in response.json()] == [1, 2] - assert response.json() == user_questions - - -def test_create_user_question_works(admin_client: TestClient): - response = admin_client.post("/admin/user-questions/") - assert response.status_code == 200 - assert response.json() == { - "id": 3, - "order": 0, - "component": "select", - "type": "text", - "options": "", - "text": { - "de": { - "options_json": "", - "user_question_id": 3, - "options": "", - "lang_id": "de", - "question": "", - }, - "en": { - "options_json": "", - "user_question_id": 3, - "options": "", - "lang_id": "en", - "question": "", - }, - "fr": { - "options_json": "", - "user_question_id": 3, - "options": "", - "lang_id": "fr", - "question": "", - }, - }, - "additional_option": "", - } - - -def test_update_user_question_works( - admin_client: TestClient, default_user_question_admin -): - response = admin_client.put( - "/admin/user-questions/", json=default_user_question_admin - ) - - assert response.status_code == 200 - - assert response.json() == default_user_question_admin - - -def test_update_user_question_id_not_there(admin_client: TestClient): - user_question_admin = { - "id": 5, - "component": "textarea", - "type": "other_thing", - "order": 0, - "options": "some_options", - "text": { - "de": { - "options_json": "", - "user_question_id": 5, - "options": "", - "lang_id": "de", - "question": "", - }, - "en": { - "options_json": "", - "user_question_id": 5, - "options": "", - "lang_id": "en", - "question": "", - }, - "fr": { - "options_json": "", - "user_question_id": 5, - "options": "", - "lang_id": "fr", - "question": "", - }, - }, - "additional_option": "nothing", - } - - response = admin_client.put("/admin/user-questions/", json=user_question_admin) - - assert response.status_code == 404 - - -def test_delete_user_question_works(session, admin_client: TestClient): - response = admin_client.delete("/admin/user-questions/1") - - assert response.status_code == 200 - assert response.json() == {"ok": True} - - user_questions = session.exec(select(UserQuestion)).all() - assert len(user_questions) == 1 - assert user_questions[0].id == 2 - - -def test_delete_user_question_id_not_there(admin_client: TestClient): - response = admin_client.delete("/admin/user-questions/12") - - assert response.status_code == 404 - - -def test_get_child_question_admin_works(admin_client: TestClient, child_questions): - response = admin_client.get("/admin/child-questions/") - - assert response.status_code == 200 - assert [element["order"] for element in response.json()] == [0, 1] - assert response.json() == child_questions - - -def test_create_child_question_works(admin_client: TestClient): - response = admin_client.post("/admin/child-questions/") - - assert response.status_code == 200 - assert response.json() == { - "id": 3, - "order": 0, - "component": "select", - "type": "text", - "options": "", - "text": { - "de": { - "options_json": "", - "child_question_id": 3, - "options": "", - "lang_id": "de", - "question": "", - }, - "en": { - "options_json": "", - "child_question_id": 3, - "options": "", - "lang_id": "en", - "question": "", - }, - "fr": { - "options_json": "", - "child_question_id": 3, - "options": "", - "lang_id": "fr", - "question": "", - }, - }, - "additional_option": "", - } - - -def test_update_child_question_works(admin_client: TestClient): - child_question_admin = { - "id": 2, - "component": "textarea", - "type": "other_thing", - "order": 0, - "options": "some_options", - "text": { - "de": { - "options_json": "", - "child_question_id": 2, - "options": "", - "lang_id": "de", - "question": "", - }, - "en": { - "options_json": "", - "child_question_id": 2, - "options": "", - "lang_id": "en", - "question": "", - }, - "fr": { - "options_json": "", - "child_question_id": 2, - "options": "", - "lang_id": "fr", - "question": "", - }, - }, - "additional_option": "nothing", - } - response = admin_client.put("/admin/child-questions", json=child_question_admin) - assert response.status_code == 200 - - -def test_update_child_question_id_not_there(admin_client: TestClient): - child_question_admin = { - "id": 5, - "component": "textarea", - "type": "other_thing", - "order": 0, - "options": "some_options", - "text": { - "de": { - "options_json": "", - "child_question_id": 5, - "options": "", - "lang_id": "de", - "question": "", - }, - "en": { - "options_json": "", - "child_question_id": 5, - "options": "", - "lang_id": "en", - "question": "", - }, - "fr": { - "options_json": "", - "child_question_id": 5, - "options": "", - "lang_id": "fr", - "question": "", - }, - }, - "additional_option": "nothing", - } - response = admin_client.put("/admin/child-questions/", json=child_question_admin) - assert response.status_code == 404 - - -def test_delete_child_question_works(session, admin_client: TestClient): - response = admin_client.delete("/admin/child-questions/1") - assert response.status_code == 200 - assert response.json() == {"ok": True} - - child_questions = session.exec(select(ChildQuestion)).all() - assert len(child_questions) == 1 - assert child_questions[0].id == 2 - - -def test_delete_child_question_id_not_there(admin_client: TestClient): - response = admin_client.delete("/admin/child-questions/12") - assert response.status_code == 404 diff --git a/mondey_backend/tests/routers/test_users.py b/mondey_backend/tests/routers/test_users.py index 2f3e08d4..10fdab1a 100644 --- a/mondey_backend/tests/routers/test_users.py +++ b/mondey_backend/tests/routers/test_users.py @@ -59,9 +59,10 @@ def test_get_child_image(user_client: TestClient): assert response.content == b"2.jpg" -def test_get_child_image_no_image(user_client: TestClient, data_dir: pathlib.Path): +def test_get_child_image_no_image(user_client: TestClient): response = user_client.get("/users/children-images/1") assert response.status_code == 200 + assert response.content == b"default_child.jpg" def test_create_update_and_delete_child(user_client: TestClient): @@ -148,8 +149,8 @@ def test_delete_child_image( assert user_client.get("/users/children-images/1").status_code == 200 -def test_get_milestone_answers_child1_user_does_not_own_child(admin_client: TestClient): - response = admin_client.get("/users/milestone-answers/1") +def test_get_milestone_answers_child1_user_does_not_own_child(user_client2: TestClient): + response = user_client2.get("/users/milestone-answers/1") assert response.status_code == 404 @@ -268,7 +269,7 @@ def test_update_current_user_answers_prexisting(user_client: TestClient): assert response.json() == publicanswers -def test_update_current_user_answers_no_prexisting(second_user_client: TestClient): +def test_update_current_user_answers_no_prexisting(user_client2: TestClient): publicanswers = [ { "answer": "other", @@ -282,7 +283,7 @@ def test_update_current_user_answers_no_prexisting(second_user_client: TestClien }, ] - response = second_user_client.put( + response = user_client2.put( "/users/user-answers/", json=publicanswers, ) @@ -290,7 +291,7 @@ def test_update_current_user_answers_no_prexisting(second_user_client: TestClien assert response.status_code == 200 assert response.json() == publicanswers - response = second_user_client.get("/users/user-answers/") + response = user_client2.get("/users/user-answers/") assert response.status_code == 200 assert response.json() == publicanswers @@ -341,9 +342,9 @@ def test_update_current_child_answers_prexisting( def test_update_current_child_answers_no_prexisting( - second_user_client: TestClient, child_answers: dict[str, str | int | None] + user_client2: TestClient, child_answers: dict[str, str | int | None] ): - response = second_user_client.put( + response = user_client2.put( "/users/children-answers/2", json=child_answers, )