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 id code for users to use when signing up
  - add checkdigit dependency to add checksum to code
- admin frontend
  - WIP users tab: add reseach group code, 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 29, 2024
1 parent 07b46c4 commit 62a1604
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 66 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.

68 changes: 67 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,18 @@ export const QuestionTextPublicSchema = {
title: 'QuestionTextPublic'
} as const;

export const ResearchGroupSchema = {
properties: {
id: {
type: 'integer',
title: 'Id'
}
},
type: 'object',
required: ['id'],
title: 'ResearchGroup'
} as const;

export const SubmittedMilestoneImagePublicSchema = {
properties: {
id: {
Expand Down Expand Up @@ -937,6 +949,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 +1134,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 +1217,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
34 changes: 34 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,10 @@ export type QuestionTextPublic = {
options?: string;
};

export type ResearchGroup = {
id: number;
};

export type SubmittedMilestoneImagePublic = {
id: number;
milestone_id: number;
Expand All @@ -235,6 +239,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 +280,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 +291,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 +589,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
31 changes: 29 additions & 2 deletions frontend/src/lib/components/Admin/Users.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<script lang="ts">
import SaveButton from "$lib/components/Admin/SaveButton.svelte";
import {
Button,
Card,
Checkbox,
Table,
Expand All @@ -13,8 +14,12 @@ import {
TableHeadCell,
} from "flowbite-svelte";
import { getUsers, usersPatchUser } from "$lib/client/services.gen";
import type { UserRead, UserUpdate } from "$lib/client/types.gen";
import {
createResearchGroup,
getUsers,
usersPatchUser,
} from "$lib/client/services.gen";
import type { UserRead } from "$lib/client/types.gen";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
Expand Down Expand Up @@ -53,6 +58,19 @@ async function updateUser(user: UserRead) {
}
}
async function addResearchGroup(user: UserRead) {
const { data, error } = await createResearchGroup({
path: {
user_id: user.id,
},
});
if (error || !data) {
console.log(error);
} else {
await refreshUsers();
}
}
onMount(async () => {
await refreshUsers();
});
Expand All @@ -68,6 +86,8 @@ onMount(async () => {
<TableHeadCell>Active</TableHeadCell>
<TableHeadCell>Verified</TableHeadCell>
<TableHeadCell>Researcher</TableHeadCell>
<TableHeadCell>Full data access</TableHeadCell>
<TableHeadCell>Research Code</TableHeadCell>
<TableHeadCell>Admin</TableHeadCell>
<TableHeadCell>{$_('admin.actions')}</TableHeadCell>
</TableHead>
Expand All @@ -86,10 +106,17 @@ onMount(async () => {
<TableBodyCell>
<Checkbox bind:checked={user.is_researcher} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<Checkbox disabled={!user.is_researcher} bind:checked={user.full_data_access} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
{user.research_group_id}
</TableBodyCell>
<TableBodyCell>
<Checkbox bind:checked={user.is_superuser} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<Button disabled={!user.is_researcher || user.research_group_id != 0} onclick={() => {addResearchGroup(user)}} >Add research code</Button>
<SaveButton disabled={saveDisabled[user.id]} onclick={() => {updateUser(user)}} />
</TableBodyCell>
</TableBodyRow>
Expand Down
Loading

0 comments on commit 62a1604

Please sign in to comment.