Skip to content

Commit

Permalink
Allow users to submit milestone images
Browse files Browse the repository at this point in the history
- backend
  - add SubmittedMilestoneImage to models
  - add /submitted-milestone-images post endpoint for users
  - add admin endpoints to get, approve and delete submitted images
- frontend
  - button to Milestone component for user to upload an image
  - add SubmitMilestoneImageModal
    - with a checkbox for user to agree to conditions before submitting the image
- admin-frontend
  - add tab with submitted images for review (accept / delete)
  - add text prop to SaveButton component
- resolves #50
  • Loading branch information
lkeegan committed Nov 22, 2024
1 parent d5bc9e6 commit 7c88c02
Show file tree
Hide file tree
Showing 20 changed files with 512 additions and 16 deletions.
33 changes: 33 additions & 0 deletions frontend/src/lib/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ export const Body_reset_reset_password_auth_reset_password_postSchema = {
title: 'Body_reset_reset_password_auth_reset_password_post'
} as const;

export const Body_submit_milestone_image_submitted_milestone_images__milestone_id__postSchema = {
properties: {
file: {
type: 'string',
format: 'binary',
title: 'File'
}
},
type: 'object',
required: ['file'],
title: 'Body_submit_milestone_image_submitted_milestone_images__milestone_id__post'
} as const;

export const Body_upload_child_image_users_children_images__child_id__putSchema = {
properties: {
file: {
Expand Down Expand Up @@ -819,6 +832,26 @@ export const QuestionTextPublicSchema = {
title: 'QuestionTextPublic'
} as const;

export const SubmittedMilestoneImagePublicSchema = {
properties: {
id: {
type: 'integer',
title: 'Id'
},
milestone_id: {
type: 'integer',
title: 'Milestone Id'
},
user_id: {
type: 'integer',
title: 'User Id'
}
},
type: 'object',
required: ['id', 'milestone_id', 'user_id'],
title: 'SubmittedMilestoneImagePublic'
} as const;

export const UserAnswerPublicSchema = {
properties: {
answer: {
Expand Down
47 changes: 46 additions & 1 deletion frontend/src/lib/client/services.gen.ts
Original file line number Diff line number Diff line change
@@ -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, OrderMilestoneGroupsAdminData, OrderMilestoneGroupsAdminError, OrderMilestoneGroupsAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, OrderMilestonesAdminData, OrderMilestonesAdminError, OrderMilestonesAdminResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, OrderUserQuestionsAdminData, OrderUserQuestionsAdminError, OrderUserQuestionsAdminResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, OrderChildQuestionsAdminData, OrderChildQuestionsAdminError, OrderChildQuestionsAdminResponse, 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';
import type { GetLanguagesError, GetLanguagesResponse, GetMilestonesError, GetMilestonesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, SubmitMilestoneImageData, SubmitMilestoneImageError, SubmitMilestoneImageResponse, GetUserQuestionsError, GetUserQuestionsResponse, GetChildQuestionsError, GetChildQuestionsResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, OrderMilestoneGroupsAdminData, OrderMilestoneGroupsAdminError, OrderMilestoneGroupsAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, OrderMilestonesAdminData, OrderMilestonesAdminError, OrderMilestonesAdminResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetSubmittedMilestoneImagesError, GetSubmittedMilestoneImagesResponse, ApproveSubmittedMilestoneImageData, ApproveSubmittedMilestoneImageError, ApproveSubmittedMilestoneImageResponse, DeleteSubmittedMilestoneImageData, DeleteSubmittedMilestoneImageError, DeleteSubmittedMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, OrderUserQuestionsAdminData, OrderUserQuestionsAdminError, OrderUserQuestionsAdminResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, OrderChildQuestionsAdminData, OrderChildQuestionsAdminError, OrderChildQuestionsAdminResponse, 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());

Expand Down Expand Up @@ -45,6 +45,21 @@ export const getMilestoneGroups = <ThrowOnError extends boolean = false>(options
});
};

/**
* Submit Milestone Image
*/
export const submitMilestoneImage = <ThrowOnError extends boolean = false>(options: Options<SubmitMilestoneImageData, ThrowOnError>) => {
return (options?.client ?? client).post<SubmitMilestoneImageResponse, SubmitMilestoneImageError, ThrowOnError>({
...options,
...formDataBodySerializer,
headers: {
'Content-Type': null,
...options?.headers
},
url: '/submitted-milestone-images/{milestone_id}'
});
};

/**
* Get User Questions
*/
Expand Down Expand Up @@ -225,6 +240,36 @@ export const deleteMilestoneImage = <ThrowOnError extends boolean = false>(optio
});
};

/**
* Get Submitted Milestone Images
*/
export const getSubmittedMilestoneImages = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => {
return (options?.client ?? client).get<GetSubmittedMilestoneImagesResponse, GetSubmittedMilestoneImagesError, ThrowOnError>({
...options,
url: '/admin/submitted-milestone-images/'
});
};

/**
* Approve Submitted Milestone Image
*/
export const approveSubmittedMilestoneImage = <ThrowOnError extends boolean = false>(options: Options<ApproveSubmittedMilestoneImageData, ThrowOnError>) => {
return (options?.client ?? client).post<ApproveSubmittedMilestoneImageResponse, ApproveSubmittedMilestoneImageError, ThrowOnError>({
...options,
url: '/admin/submitted-milestone-images/approve/{submitted_milestone_image_id}'
});
};

/**
* Delete Submitted Milestone Image
*/
export const deleteSubmittedMilestoneImage = <ThrowOnError extends boolean = false>(options: Options<DeleteSubmittedMilestoneImageData, ThrowOnError>) => {
return (options?.client ?? client).delete<DeleteSubmittedMilestoneImageResponse, DeleteSubmittedMilestoneImageError, ThrowOnError>({
...options,
url: '/admin/submitted-milestone-images/{submitted_milestone_image_id}'
});
};

/**
* Get Milestone Age Scores
*/
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/lib/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export type Body_reset_reset_password_auth_reset_password_post = {
password: string;
};

export type Body_submit_milestone_image_submitted_milestone_images__milestone_id__post = {
file: (Blob | File);
};

export type Body_upload_child_image_users_children_images__child_id__put = {
file: (Blob | File);
};
Expand Down Expand Up @@ -212,6 +216,12 @@ export type QuestionTextPublic = {
options?: string;
};

export type SubmittedMilestoneImagePublic = {
id: number;
milestone_id: number;
user_id: number;
};

export type UserAnswerPublic = {
answer: string;
additional_answer: (string | null);
Expand Down Expand Up @@ -309,6 +319,17 @@ export type GetMilestoneGroupsResponse = (Array<MilestoneGroupPublic>);

export type GetMilestoneGroupsError = (HTTPValidationError);

export type SubmitMilestoneImageData = {
body: Body_submit_milestone_image_submitted_milestone_images__milestone_id__post;
path: {
milestone_id: number;
};
};

export type SubmitMilestoneImageResponse = (unknown);

export type SubmitMilestoneImageError = (HTTPValidationError);

export type GetUserQuestionsResponse = (Array<UserQuestionPublic>);

export type GetUserQuestionsError = unknown;
Expand Down Expand Up @@ -452,6 +473,30 @@ export type DeleteMilestoneImageResponse = (unknown);

export type DeleteMilestoneImageError = (HTTPValidationError);

export type GetSubmittedMilestoneImagesResponse = (Array<SubmittedMilestoneImagePublic>);

export type GetSubmittedMilestoneImagesError = unknown;

export type ApproveSubmittedMilestoneImageData = {
path: {
submitted_milestone_image_id: number;
};
};

export type ApproveSubmittedMilestoneImageResponse = (unknown);

export type ApproveSubmittedMilestoneImageError = (HTTPValidationError);

export type DeleteSubmittedMilestoneImageData = {
path: {
submitted_milestone_image_id: number;
};
};

export type DeleteSubmittedMilestoneImageResponse = (unknown);

export type DeleteSubmittedMilestoneImageError = (HTTPValidationError);

export type GetMilestoneAgeScoresData = {
path: {
milestone_id: number;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Admin/Languages.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function deleteLanguageAndUpdateLanguages() {
<TableHead>
<TableHeadCell>Code (ISO 639-1)</TableHeadCell>
<TableHeadCell>Name</TableHeadCell>
<TableHeadCell>Actions</TableHeadCell>
<TableHeadCell>{$_('admin.actions')}</TableHeadCell>
</TableHead>
<TableBody>
{#each $locales as lang_id}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Admin/Questions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ onMount(async () => {
<TableHeadCell>Question</TableHeadCell>
<TableHeadCell>Input type</TableHeadCell>
<TableHeadCell>Options</TableHeadCell>
<TableHeadCell>Actions</TableHeadCell>
<TableHeadCell>{$_('admin.actions')}</TableHeadCell>
</TableHead>
<TableBody>
{#each $questions as question, groupIndex (question.id)}
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/lib/components/Admin/SaveButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import CheckOutline from "flowbite-svelte-icons/CheckOutline.svelte";
import Button from "flowbite-svelte/Button.svelte";
import { _ } from "svelte-i18n";
let { onclick, disabled = false }: { onclick: () => void; disabled?: boolean } =
$props();
let {
onclick,
text = $_("admin.save-changes"),
disabled = false,
}: { onclick: () => void; text?: string; disabled?: boolean } = $props();
</script>

<Button color="green" {onclick} {disabled}
><CheckOutline class="me-2 h-5 w-5" /> {$_('admin.save-changes')}</Button
><CheckOutline class="me-2 h-5 w-5" /> {text}</Button
>
116 changes: 116 additions & 0 deletions frontend/src/lib/components/Admin/SubmittedMilestoneImages.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<svelte:options runes={true}/>

<script lang="ts">
import { refreshMilestoneGroups } from "$lib/admin.svelte";
import {
approveSubmittedMilestoneImage,
deleteSubmittedMilestoneImage,
getSubmittedMilestoneImages,
} from "$lib/client/services.gen";
import type { SubmittedMilestoneImagePublic } from "$lib/client/types.gen";
import DeleteButton from "$lib/components/Admin/DeleteButton.svelte";
import DeleteModal from "$lib/components/Admin/DeleteModal.svelte";
import SaveButton from "$lib/components/Admin/SaveButton.svelte";
import { milestoneGroups } from "$lib/stores/adminStore";
import {
Card,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
} from "flowbite-svelte";
import { onMount } from "svelte";
import { _, locale } from "svelte-i18n";
let images = $state([] as Array<SubmittedMilestoneImagePublic>);
let currentImageId = $state(0);
let showDeleteModal: boolean = $state(false);
async function refreshImages() {
const { data, error } = await getSubmittedMilestoneImages();
if (error || !data) {
console.log(error);
} else {
images = data;
}
}
async function deleteCurrentImage() {
const { data, error } = await deleteSubmittedMilestoneImage({
path: {
submitted_milestone_image_id: currentImageId,
},
});
if (error || !data) {
console.log(error);
} else {
await refreshImages();
}
}
async function approveImage(image_id: number) {
const { data, error } = await approveSubmittedMilestoneImage({
path: {
submitted_milestone_image_id: image_id,
},
});
if (error || !data) {
console.log(error);
} else {
await refreshImages();
await refreshMilestoneGroups();
}
}
onMount(async () => {
await refreshImages();
});
</script>

{#if $locale}
<Card size="xl" class="m-5 w-full">
<h3 class="mb-3 text-xl font-medium text-gray-900 dark:text-white">
{$_("admin.users")}
</h3>
<Table>
<TableHead>
<TableHeadCell>{$_('admin.milestone')}</TableHeadCell>
<TableHeadCell>{$_('admin.image')}</TableHeadCell>
<TableHeadCell>{$_('admin.actions')}</TableHeadCell>
</TableHead>
<TableBody>
{#each $milestoneGroups as milestoneGroup (milestoneGroup.id)}
{@const groupTitle = milestoneGroup.text[$locale].title}
{#each milestoneGroup.milestones as milestone (milestone.id)}
{@const milestoneTitle = `${groupTitle} / ${milestone.text[$locale].title}`}
{#each images as image (image.id)}
{#if image.milestone_id === milestone.id}
<TableBodyRow>
<TableBodyCell>
{milestoneTitle}
</TableBodyCell>
<TableBodyCell>
<img src={`${import.meta.env.VITE_MONDEY_API_URL}/static/ms/${image.id}.webp`}
alt={`${image.id}`}/>
</TableBodyCell>
<TableBodyCell>
<SaveButton text={$_("admin.approve")} onclick={() => {approveImage(image.id)}}/>
<DeleteButton onclick={() => {
currentImageId = image.id;
showDeleteModal = true;
}}
/>
</TableBodyCell>
</TableBodyRow>
{/if}
{/each}
{/each}
{/each}
</TableBody>
</Table>
</Card>
{/if}

<DeleteModal bind:open={showDeleteModal} onclick={deleteCurrentImage}></DeleteModal>
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Admin/Users.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ onMount(async () => {
<TableHeadCell>Verified</TableHeadCell>
<TableHeadCell>Researcher</TableHeadCell>
<TableHeadCell>Admin</TableHeadCell>
<TableHeadCell>Actions</TableHeadCell>
<TableHeadCell>{$_('admin.actions')}</TableHeadCell>
</TableHead>
<TableBody>
{#each users as user (user.id)}
Expand Down
Loading

0 comments on commit 7c88c02

Please sign in to comment.