From 84c3f171cc5e6a39c5123fbbb70746cbd33b04fa Mon Sep 17 00:00:00 2001 From: Harald Mack Date: Mon, 11 Nov 2024 09:03:59 +0100 Subject: [PATCH] Squashed commit of the following: commit 43bf1b3c6972338c82e1885e7f347fc9892f0623 Author: Liam Keegan Date: Mon Nov 11 08:17:47 2024 +0100 Add admin option to delete milestone images (#150) * Add admin option to delete milestone images - add delete button below existing milestone images in admin interface - add delete /milestone-images endpoint - restate /admin route for convenience during development, can be removed again later - resolves #98 * update openapi.json & openapi-ts client --------- Co-authored-by: Harald Mack <39521902+MaHaWo@users.noreply.github.com> Co-authored-by: github-actions[bot] commit 33d4f17f248a666de8ad70868f27a06e4f09e176 Author: Harald Mack <39521902+MaHaWo@users.noreply.github.com> Date: Fri Nov 8 17:23:58 2024 +0100 Add childquestions backend integration and refactor datainput datastructure (#138) * add childanswers and base answer model * add child question models * add question admin endpoints * make current tests work again * adjust imports, add test skeleton * add dummy questions to test fixtures * start working on backend tests * work more on backend tests and model inheritance * fix errors in test data * try to get sqlmodel inheritnace to work * get rid of inheritance in question models for now * finish user question tests * finish python test draft --- docker-compose.yml | 2 + frontend/src/lib/client/services.gen.ts | 12 +++- frontend/src/lib/client/types.gen.ts | 10 ++++ .../src/lib/components/Admin/EditImage.svelte | 23 ++++++++ .../Admin/EditMilestoneModal.svelte | 53 +++++++++++++----- .../components/Admin/MilestoneGroups.svelte | 2 +- .../src/lib/components/Admin/Questions.svelte | 6 +- .../src/lib/components/ChildrenGallery.svelte | 44 +++++++++------ .../components/ChildrenRegistration.svelte | 6 +- frontend/src/routes/admin/+page.svelte | 7 +++ mondey_backend/data/dummy.jpg | Bin 0 -> 17801 bytes mondey_backend/openapi.json | 2 +- .../src/mondey_backend/routers/admin.py | 10 ++++ .../src/mondey_backend/routers/users.py | 15 ++--- mondey_backend/src/mondey_backend/settings.py | 1 + mondey_backend/tests/conftest.py | 14 ++++- mondey_backend/tests/routers/test_admin.py | 23 +++++++- mondey_backend/tests/routers/test_users.py | 26 ++------- 18 files changed, 180 insertions(+), 76 deletions(-) create mode 100644 frontend/src/lib/components/Admin/EditImage.svelte create mode 100644 frontend/src/routes/admin/+page.svelte create mode 100644 mondey_backend/data/dummy.jpg diff --git a/docker-compose.yml b/docker-compose.yml index 34ef1ae0..1d6e257a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,9 @@ 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 - ENABLE_CORS=${ENABLE_CORS:-false} - HOST=${HOST:-backend} diff --git a/frontend/src/lib/client/services.gen.ts b/frontend/src/lib/client/services.gen.ts index 4fac64ce..88ad465d 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, 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 Options, createClient, createConfig, formDataBodySerializer, urlSearchParamsBodySerializer } from '@hey-api/client-fetch'; +import type { AuthCookieLoginData, AuthCookieLoginError, AuthCookieLoginResponse, AuthCookieLogoutError, AuthCookieLogoutResponse, AuthError, AuthResponse, CreateChildData, CreateChildError, CreateChildQuestionError, CreateChildQuestionResponse, CreateChildResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, CreateMilestoneResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteChildData, DeleteChildError, DeleteChildImageData, DeleteChildImageError, DeleteChildImageResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, DeleteChildResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, DeleteMilestoneResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, GetChildData, GetChildError, GetChildImageData, GetChildImageError, GetChildImageResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, GetChildQuestionsError, GetChildQuestionsResponse, GetChildResponse, GetChildrenError, GetChildrenResponse, GetCurrentChildAnswersData, GetCurrentChildAnswersError, GetCurrentChildAnswersResponse, GetCurrentMilestoneAnswerSessionData, GetCurrentMilestoneAnswerSessionError, GetCurrentMilestoneAnswerSessionResponse, GetCurrentUserAnswersError, GetCurrentUserAnswersResponse, GetLanguagesError, GetLanguagesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, GetMilestoneResponse, GetMilestonesError, GetMilestonesResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, GetUserQuestionsError, GetUserQuestionsResponse, RegisterRegisterData, RegisterRegisterError, RegisterRegisterResponse, ResetForgotPasswordData, ResetForgotPasswordError, ResetForgotPasswordResponse, ResetResetPasswordData, ResetResetPasswordError, ResetResetPasswordResponse, UpdateChildData, UpdateChildError, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, UpdateChildResponse, UpdateCurrentChildAnswersData, UpdateCurrentChildAnswersError, UpdateCurrentChildAnswersResponse, UpdateCurrentUserAnswersData, UpdateCurrentUserAnswersError, UpdateCurrentUserAnswersResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, UpdateMilestoneAnswerData, UpdateMilestoneAnswerError, UpdateMilestoneAnswerResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, UpdateMilestoneResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, UploadChildImageData, UploadChildImageError, UploadChildImageResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, UsersCurrentUserError, UsersCurrentUserResponse, UsersDeleteUserData, UsersDeleteUserError, UsersDeleteUserResponse, UsersPatchCurrentUserData, UsersPatchCurrentUserError, UsersPatchCurrentUserResponse, UsersPatchUserData, UsersPatchUserError, UsersPatchUserResponse, UsersUserData, UsersUserError, UsersUserResponse, VerifyRequestTokenData, VerifyRequestTokenError, VerifyRequestTokenResponse, VerifyVerifyData, VerifyVerifyError, VerifyVerifyResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -159,6 +159,14 @@ export const uploadMilestoneImage = (optio url: '/admin/milestone-images/{milestone_id}' }); }; +/** + * Delete Milestone Image + */ +export const deleteMilestoneImage = (options: Options) => { return (options?.client ?? client).delete({ + ...options, + url: '/admin/milestone-images/{milestone_image_id}' +}); }; + /** * Get User Questions Admin */ diff --git a/frontend/src/lib/client/types.gen.ts b/frontend/src/lib/client/types.gen.ts index a51c7657..eac88dbe 100644 --- a/frontend/src/lib/client/types.gen.ts +++ b/frontend/src/lib/client/types.gen.ts @@ -413,6 +413,16 @@ export type UploadMilestoneImageResponse = (MilestoneImage); export type UploadMilestoneImageError = (HTTPValidationError); +export type DeleteMilestoneImageData = { + path: { + milestone_image_id: number; + }; +}; + +export type DeleteMilestoneImageResponse = (unknown); + +export type DeleteMilestoneImageError = (HTTPValidationError); + export type GetUserQuestionsAdminResponse = (Array); export type GetUserQuestionsAdminError = unknown; diff --git a/frontend/src/lib/components/Admin/EditImage.svelte b/frontend/src/lib/components/Admin/EditImage.svelte new file mode 100644 index 00000000..32621564 --- /dev/null +++ b/frontend/src/lib/components/Admin/EditImage.svelte @@ -0,0 +1,23 @@ + + + + +
+{`${filename}`} + +
diff --git a/frontend/src/lib/components/Admin/EditMilestoneModal.svelte b/frontend/src/lib/components/Admin/EditMilestoneModal.svelte index a2997e35..72d9f551 100644 --- a/frontend/src/lib/components/Admin/EditMilestoneModal.svelte +++ b/frontend/src/lib/components/Admin/EditMilestoneModal.svelte @@ -3,12 +3,17 @@ - + {#if milestone} {#each textKeys as textKey} {@const title = $_(`admin.${textKey}`)} @@ -83,17 +108,17 @@ export async function saveChanges() {
- {#each milestone.images as milestoneImage, milestoneImageId (milestoneImage.id)} - {`${milestoneImageId}`} { + currentMilestoneImageId = milestoneImage.id; + showDeleteMilestoneImageModal = true; + }} /> {/each} {#each images as image} - milestone + milestone {/each}
{/if} - - + {open = false; saveChanges()}} /> + {open = false;}} /> + + diff --git a/frontend/src/lib/components/Admin/MilestoneGroups.svelte b/frontend/src/lib/components/Admin/MilestoneGroups.svelte index 9128e795..7b2fe86e 100644 --- a/frontend/src/lib/components/Admin/MilestoneGroups.svelte +++ b/frontend/src/lib/components/Admin/MilestoneGroups.svelte @@ -251,7 +251,7 @@ onMount(async () => { > {#key showEditMilestoneModal} - {/key} diff --git a/frontend/src/lib/components/Admin/Questions.svelte b/frontend/src/lib/components/Admin/Questions.svelte index 24493ed6..78b73462 100644 --- a/frontend/src/lib/components/Admin/Questions.svelte +++ b/frontend/src/lib/components/Admin/Questions.svelte @@ -28,7 +28,7 @@ import DeleteModal from "$lib/components/Admin/DeleteModal.svelte"; import EditButton from "$lib/components/Admin/EditButton.svelte"; import EditQuestionModal from "$lib/components/Admin/EditQuestionModal.svelte"; import { childQuestions, userQuestions } from "$lib/stores/adminStore"; -import { type Component, onMount } from "svelte"; +import { onMount } from "svelte"; import { _, locale } from "svelte-i18n"; import type { Writable } from "svelte/store"; @@ -43,7 +43,6 @@ let create: any; let doDelete: any; let refresh: any; let build: any; -let component: Component = EditQuestionModal; let questions: | Writable> | Writable> @@ -155,8 +154,7 @@ onMount(async () => { {#key showEditQuestionModal} - { const childrenData = await Promise.all( children.data.map(async (child) => { let image = null; - if (child.has_image) { - const childImageResponse = await getChildImage({ - path: { child_id: child.id }, + const childImageResponse = await getChildImage({ + path: { child_id: child.id }, + }); + console.log("childImageResponse", childImageResponse); + if (childImageResponse.error) { + console.log("Error when retrieving child image"); + showAlert = true; + alertMessage = + $_("childData.alertMessageImage") + + " " + + childImageResponse.error.detail; + } else { + const reader = new FileReader(); + reader.readAsDataURL(childImageResponse.data); + image = await new Promise((resolve) => { + reader.onloadend = () => resolve(reader.result as string); }); - if (childImageResponse.error) { - console.log("Error when retrieving child image"); - showAlert = true; - alertMessage = - $_("childData.alertMessageImage") + - childImageResponse.error.detail; - } else { - const reader = new FileReader(); - reader.readAsDataURL(childImageResponse.data); - image = await new Promise((resolve) => { - reader.onloadend = () => resolve(reader.result as string); - }); - } } return { header: child.name, @@ -56,6 +56,7 @@ async function setup(): Promise { // add the 'new child' card as the first element data = [ + ...childrenData, { header: $_("childData.newChildHeading"), summary: $_("childData.newChildHeadingLong"), @@ -67,7 +68,6 @@ async function setup(): Promise { }, image: null, }, - ...childrenData, ]; } return data; @@ -129,6 +129,16 @@ const searchData = [ {#await promise}

{"Waiting for server response"}

{:then data} + {#if showAlert} + { + showAlert = false; + }} + /> + {/if} +
{ } async function submitData(): Promise { - // handle image data - await submitImageData(); - // submit child data await submitChildData(); + // handle image data + await submitImageData(); + // disable all elements to make editing a conscious choice amd go back to childrenGallery console.log("submission of child data successful."); activeTabChildren.set("childrenGallery"); diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte new file mode 100644 index 00000000..9e5443a4 --- /dev/null +++ b/frontend/src/routes/admin/+page.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/mondey_backend/data/dummy.jpg b/mondey_backend/data/dummy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..589e0f049453b2fb7d625327a4a193c28df3c7ba GIT binary patch literal 17801 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