Skip to content

Commit

Permalink
Add researcher codes
Browse files Browse the repository at this point in the history
- backend
  - add ResearchGroup model with 6-digit code for users to use when signing up
  - add checkdigit dependency to add checksum to code
  - TODO: maybe make the code the id? otherwise have to modify user sign up logic
  - TODO: exclude leading 0s to ensure it remains 6 digits even if converted to an int?
- admin frontend
  - TODO add research groups tab (or maybe add the table to the users tab?)
  - TODO users tab: add button on user to create research group, allow to filter users by research group
- frontend
  - add ResearchCodeInput input which checks the entered code checksum is valid
  - WIP add optional code input to user signup, remove role selection
  - WIP can add code in url to send sign up links to users with code pre-filled (make route /signup?)
  - add cdigit dependency to validate codes
- resolves #185
  • Loading branch information
lkeegan committed Nov 27, 2024
1 parent 07b46c4 commit 478549d
Show file tree
Hide file tree
Showing 17 changed files with 322 additions and 33 deletions.
4 changes: 3 additions & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ WORKDIR /app

COPY package*.json ./

COPY pnpm-lock.yaml ./

RUN npm install -g pnpm && pnpm install

COPY . .

RUN echo "VITE_MONDEY_API_URL=${MONDEY_API_URL}" > .env && pnpm run build

FROM nginx:1.27.1
FROM nginx:1.27.3

COPY --from=builder /app/build /usr/share/nginx/html

Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"dependencies": {
"@hey-api/client-fetch": "0.4.3",
"@unovis/ts": "1.5.0-beta.0",
"cdigit": "^4.0.2",
"iso-639-1": "3.1.3",
"svelte-dnd-action": "^0.9.52",
"svelte-i18n": "^4.0.1"
Expand Down
17 changes: 17 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 79 additions & 1 deletion frontend/src/lib/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,30 @@ export const QuestionTextPublicSchema = {
title: 'QuestionTextPublic'
} as const;

export const ResearchGroupSchema = {
properties: {
id: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Id'
},
code: {
type: 'string',
maxLength: 6,
title: 'Code'
}
},
type: 'object',
required: ['code'],
title: 'ResearchGroup'
} as const;

export const SubmittedMilestoneImagePublicSchema = {
properties: {
id: {
Expand Down Expand Up @@ -937,6 +961,30 @@ export const UserCreateSchema = {
],
title: 'Is Researcher',
default: false
},
full_data_access: {
anyOf: [
{
type: 'boolean'
},
{
type: 'null'
}
],
title: 'Full Data Access',
default: false
},
research_group_id: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Research Group Id',
default: 0
}
},
type: 'object',
Expand Down Expand Up @@ -1098,10 +1146,18 @@ export const UserReadSchema = {
is_researcher: {
type: 'boolean',
title: 'Is Researcher'
},
full_data_access: {
type: 'boolean',
title: 'Full Data Access'
},
research_group_id: {
type: 'integer',
title: 'Research Group Id'
}
},
type: 'object',
required: ['id', 'email', 'is_researcher'],
required: ['id', 'email', 'is_researcher', 'full_data_access', 'research_group_id'],
title: 'UserRead'
} as const;

Expand Down Expand Up @@ -1173,6 +1229,28 @@ export const UserUpdateSchema = {
}
],
title: 'Is Researcher'
},
full_data_access: {
anyOf: [
{
type: 'boolean'
},
{
type: 'null'
}
],
title: 'Full Data Access'
},
research_group_id: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Research Group Id'
}
},
type: 'object',
Expand Down
32 changes: 31 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, 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';
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, GetResearchGroupsError, GetResearchGroupsResponse, CreateResearchGroupData, CreateResearchGroupError, CreateResearchGroupResponse, DeleteResearchGroupData, DeleteResearchGroupError, DeleteResearchGroupResponse, 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 @@ -390,6 +390,36 @@ export const getUsers = <ThrowOnError extends boolean = false>(options?: Options
});
};

/**
* Get Research Groups
*/
export const getResearchGroups = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => {
return (options?.client ?? client).get<GetResearchGroupsResponse, GetResearchGroupsError, ThrowOnError>({
...options,
url: '/admin/research-groups/'
});
};

/**
* Create Research Group
*/
export const createResearchGroup = <ThrowOnError extends boolean = false>(options: Options<CreateResearchGroupData, ThrowOnError>) => {
return (options?.client ?? client).post<CreateResearchGroupResponse, CreateResearchGroupError, ThrowOnError>({
...options,
url: '/admin/research-groups/{user_id}'
});
};

/**
* Delete Research Group
*/
export const deleteResearchGroup = <ThrowOnError extends boolean = false>(options: Options<DeleteResearchGroupData, ThrowOnError>) => {
return (options?.client ?? client).delete<DeleteResearchGroupResponse, DeleteResearchGroupError, ThrowOnError>({
...options,
url: '/admin/research-groups/{research_group_id}'
});
};

/**
* Users:Current User
*/
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/lib/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ export type QuestionTextPublic = {
options?: string;
};

export type ResearchGroup = {
id?: (number | null);
code: string;
};

export type SubmittedMilestoneImagePublic = {
id: number;
milestone_id: number;
Expand All @@ -235,6 +240,8 @@ export type UserCreate = {
is_superuser?: (boolean | null);
is_verified?: (boolean | null);
is_researcher?: (boolean | null);
full_data_access?: (boolean | null);
research_group_id?: (number | null);
};

export type UserQuestionAdmin = {
Expand Down Expand Up @@ -274,6 +281,8 @@ export type UserRead = {
is_superuser?: boolean;
is_verified?: boolean;
is_researcher: boolean;
full_data_access: boolean;
research_group_id: number;
};

export type UserUpdate = {
Expand All @@ -283,6 +292,8 @@ export type UserUpdate = {
is_superuser?: (boolean | null);
is_verified?: (boolean | null);
is_researcher?: (boolean | null);
full_data_access?: (boolean | null);
research_group_id?: (number | null);
};

export type ValidationError = {
Expand Down Expand Up @@ -579,6 +590,30 @@ export type GetUsersResponse = (Array<UserRead>);

export type GetUsersError = unknown;

export type GetResearchGroupsResponse = (Array<ResearchGroup>);

export type GetResearchGroupsError = unknown;

export type CreateResearchGroupData = {
path: {
user_id: number;
};
};

export type CreateResearchGroupResponse = (ResearchGroup);

export type CreateResearchGroupError = (HTTPValidationError);

export type DeleteResearchGroupData = {
path: {
research_group_id: number;
};
};

export type DeleteResearchGroupResponse = (unknown);

export type DeleteResearchGroupError = (HTTPValidationError);

export type UsersCurrentUserResponse = (UserRead);

export type UsersCurrentUserError = (unknown);
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/lib/components/DataInput/ResearchCodeInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<svelte:options runes={true} />

<script lang="ts">
import { verhoeff } from "cdigit";
import { Input } from "flowbite-svelte";
import {
CheckCircleOutline,
ExclamationCircleOutline,
} from "flowbite-svelte-icons";
let {
value = $bindable(""),
valid = $bindable(false),
}: {
value: string;
valid: boolean;
} = $props();
function validate(event: Event) {
const target = event.target as HTMLInputElement;
valid =
target.value === "" ||
(target.value.length === 6 && verhoeff.validate(target.value));
}
</script>

<div class="flex flex-row items-center">
<Input type="text" bind:value class="mr-2" oninput={validate} color={valid ? 'green' : 'red'}/>
{#if valid}
<CheckCircleOutline color="green"/>
{:else}
<ExclamationCircleOutline color="red"/>
{/if}
</div>
Loading

0 comments on commit 478549d

Please sign in to comment.