Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add POST checkin form endpoint #126

Merged
merged 28 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cb46418
update prisma schema for checkin forms
cherylli Mar 1, 2024
aa3d913
upgrade prisma client
cherylli Mar 2, 2024
e3b386e
add sorting by order to form queries
cherylli Mar 2, 2024
dfd9067
[WIP] add check in form response seed
cherylli Mar 4, 2024
1232971
merge dev into branch
cherylli Mar 21, 2024
79fed95
fix PATCH meetings/{meetingId}/forms/{formId}
cherylli Mar 22, 2024
cd89d0e
Merge branch 'dev' of github.com:chingu-x/chingu-dashboard-be into fe…
cherylli Mar 23, 2024
038bb1e
[WIP] check in form create endpoint
cherylli Mar 24, 2024
94242ae
fix unit tests where mocked req doesn't match new CustomRequest type
cherylli Mar 24, 2024
a1a97b6
[WIP] POST sprint/checkin - add basic insert
cherylli Mar 24, 2024
0ed061b
refactor some responses related code, move them to global
cherylli Mar 24, 2024
bc07502
add api responses
cherylli Mar 26, 2024
d668c7a
update readme with FormInputValidationPipe()
cherylli Mar 27, 2024
ebf887a
add check in form 201 e2e test
cherylli Mar 27, 2024
196eaf3
update sprint e2e tests to use cookie instead of bearer token, remove…
cherylli Mar 27, 2024
5bc6c80
add all tests to POST checkin endpoints
cherylli Mar 29, 2024
4557546
add ApiOperation to sprint check in form POST endpoint
cherylli Mar 29, 2024
8440add
Merge branch 'dev' of github.com:chingu-x/chingu-dashboard-be into fe…
cherylli Mar 30, 2024
aa24dbb
resolve conflict
cherylli Mar 30, 2024
64d1fe7
resolve conflict in users.controller
cherylli Mar 30, 2024
ff4f938
generate prisma migration
cherylli Mar 30, 2024
2c40d65
add seed data for check in form
cherylli Mar 31, 2024
78213eb
add seed data for checkin form
cherylli Mar 31, 2024
23002d1
update test to fix tests due to removing unique(response + questionId…
cherylli Apr 1, 2024
1217319
regenerate prisma migration
cherylli Apr 1, 2024
50ba46f
remove new prisma client instances in seed files
cherylli Apr 1, 2024
d31075a
Merge branch 'dev' of github.com:chingu-x/chingu-dashboard-be into fe…
cherylli Apr 3, 2024
aff945f
Merge branch 'dev' into feature/checkin-form-endpoints
cherylli Apr 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Another example [here](https://co-pilot.dev/changelog)
- Add new endpoint to revoke refresh token ([#116](https://github.com/chingu-x/chingu-dashboard-be/pull/116))
- Add meetingId to sprints/teams endpoint (([#119](https://github.com/chingu-x/chingu-dashboard-be/pull/119)))
- Add new endpoint to select tech stack items ([#125](https://github.com/chingu-x/chingu-dashboard-be/pull/125))
- Add check in form response table, seed data, POST endpoint for submitting check in form ([#126](https://github.com/chingu-x/chingu-dashboard-be/pull/126))
- Add multiple device support ([#128](https://github.com/chingu-x/chingu-dashboard-be/pull/128))

### Changed
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ $ yarn docker:down
$ yarn docker:clean
```

## Custom Pipes
### FormInputValidationPipe
For use with form responses, this pipe validates that the responses or response (array) values include a questionId and at least one input value of any type.

Example: `@Body(new FormInputValidationPipe())`

## Custom Decorators

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.11",
"@prisma/client": "^5.3.1",
"@prisma/client": "^5.10.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- DropIndex
DROP INDEX "Response_questionId_responseGroupId_key";

-- CreateTable
CREATE TABLE "FormResponseCheckin" (
"id" SERIAL NOT NULL,
"voyageTeamMemberId" INTEGER NOT NULL,
"sprintId" INTEGER NOT NULL,
"adminComments" TEXT,
"feedbackSent" BOOLEAN NOT NULL DEFAULT false,
"responseGroupId" INTEGER NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "FormResponseCheckin_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "FormResponseCheckin_responseGroupId_key" ON "FormResponseCheckin"("responseGroupId");

-- CreateIndex
CREATE UNIQUE INDEX "FormResponseCheckin_voyageTeamMemberId_sprintId_key" ON "FormResponseCheckin"("voyageTeamMemberId", "sprintId");

-- AddForeignKey
ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_voyageTeamMemberId_fkey" FOREIGN KEY ("voyageTeamMemberId") REFERENCES "VoyageTeamMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_sprintId_fkey" FOREIGN KEY ("sprintId") REFERENCES "Sprint"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormResponseCheckin" ADD CONSTRAINT "FormResponseCheckin_responseGroupId_fkey" FOREIGN KEY ("responseGroupId") REFERENCES "ResponseGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
37 changes: 27 additions & 10 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ model Sprint {
updatedAt DateTime @updatedAt

teamMeetings TeamMeeting[]
checkinForms FormResponseCheckin[]

@@unique(fields: [voyageId, number], name: "voyageSprintNumber")
}
Expand Down Expand Up @@ -221,6 +222,7 @@ model VoyageTeamMember {
projectIdeaVotes ProjectIdeaVote[]
teamResources TeamResource[]
projectFeatures ProjectFeature[]
checkinForms FormResponseCheckin[]

@@unique(fields: [userId, voyageTeamId], name: "userVoyageId")
}
Expand Down Expand Up @@ -532,8 +534,6 @@ model Response {

createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt

@@unique(fields: [questionId, responseGroupId], name: "questionResponseGroup")
}

model ResponseGroup {
Expand All @@ -546,20 +546,37 @@ model ResponseGroup {
soloProject SoloProject?
voyageApplication VoyageApplication?
formResponseMeeting FormResponseMeeting?
formResponseCheckin FormResponseCheckin?
}

model FormResponseMeeting {
id Int @id @default(autoincrement())
formId Int
form Form @relation(fields: [formId], references: [id])
meetingId Int
meeting TeamMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
formId Int
form Form @relation(fields: [formId], references: [id])
meetingId Int
meeting TeamMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
responseGroupId Int? @unique
responseGroup ResponseGroup? @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)

createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt

responseGroupId Int? @unique
responseGroup ResponseGroup? @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)

@@unique(fields: [formId, meetingId], name: "meetingFormId")
}

model FormResponseCheckin {
id Int @id @default(autoincrement())
voyageTeamMemberId Int
voyageTeamMember VoyageTeamMember @relation(fields: [voyageTeamMemberId], references: [id], onDelete: Cascade)
sprintId Int
sprint Sprint @relation(fields: [sprintId], references: [id], onDelete: Cascade)
adminComments String?
feedbackSent Boolean @default(false)
responseGroupId Int @unique
responseGroup ResponseGroup @relation(fields: [responseGroupId], references: [id], onDelete: Restrict)

createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt

@@unique(fields: [voyageTeamMemberId, sprintId], name: "voyageTeamMemberSprintId")
}
234 changes: 234 additions & 0 deletions prisma/seed/checkinform-responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { formSelect } from "../../src/forms/forms.service";
import { prisma } from "./prisma-client";

// TODO: move these to a helper function file
const getRandomOptionId = async (
optionGroupId: number,
numberOfChoices: number,
) => {
const choicesArray = [];
const choices = await prisma.optionChoice.findMany({
where: {
optionGroupId,
},
});
while (choicesArray.length < numberOfChoices) {
const choice = choices[Math.floor(Math.random() * choices.length)].id;
if (!choicesArray.includes(choice)) {
choicesArray.push(choice);
}
}
return choicesArray;
};

const getTeamMembers = async (teamMemberId: number) => {
const team = await prisma.voyageTeam.findFirst({
where: {
voyageTeamMembers: {
some: {
id: teamMemberId,
},
},
},
select: {
voyageTeamMembers: {
select: {
member: {
select: {
discordId: true,
},
},
},
},
},
});
return team.voyageTeamMembers.map((m) => m.member.discordId);
};

const populateQuestionResponses = async (
question: any,
teamMemberId: number,
responseGroupId: number,
) => {
const data: any = {
questionId: question.id,
responseGroupId: responseGroupId,
};
switch (question.inputType.name) {
case "text": {
await prisma.response.create({
data: {
...data,
text: `Text response for Question id ${question.id}.`,
},
});
break;
}
case "radio": {
const radioChoices = await getRandomOptionId(
question.optionGroupId,
1,
);
await prisma.response.create({
data: {
...data,
optionChoiceId: radioChoices[0],
},
});
break;
}
case "radioGroup": {
// get all subquestions
const subQuestions = await prisma.question.findMany({
where: {
parentQuestionId: question.id,
},
select: {
id: true,
inputType: {
select: {
name: true,
},
},
optionGroupId: true,
},
});
// popuate all subquestions
await Promise.all(
subQuestions.map((subq) => {
// assign subquestion optionGroupId to be same as parent, as it's null for subquestions
subq.optionGroupId = question.optionGroupId;
populateQuestionResponses(
subq,
teamMemberId,
responseGroupId,
);
}),
);
break;
}
case "checkbox": {
// check 2 randomly,
// 2 response entries as scalar list or optional list is not supported for relations
const checkboxChoices = await getRandomOptionId(
question.optionGroupId,
2,
);

await prisma.response.createMany({
data: [
{
...data,
optionChoiceId: checkboxChoices[0],
},
{
...data,
optionChoiceId: checkboxChoices[1],
},
],
});
break;
}
case "teamMembersCheckbox": {
const selectedTeamMembers = await getTeamMembers(teamMemberId);
await prisma.response.create({
data: {
...data,
text: selectedTeamMembers
.filter((m) => m !== null)
.join(";"),
},
});
break;
}
case "number": {
await prisma.response.create({
data: {
...data,
numeric: Math.floor(Math.random() * 100),
},
});
break;
}
case "yesNo": {
await prisma.response.create({
data: {
...data,
boolean: Math.random() > 0.5,
},
});
break;
}
default: {
throw new Error("Prisma seed: Unexpected question type");
}
}
};

export const populateCheckinFormResponse = async () => {
const teamMemberId = 1;
const teamMember = await prisma.voyageTeamMember.findUnique({
where: {
id: teamMemberId,
},
select: {
id: true,
voyageTeam: {
select: {
voyage: {
select: {
sprints: true,
},
},
},
},
},
});

// get questions
const checkinForm = await prisma.form.findUnique({
where: {
title: "Sprint Check-in",
},
select: formSelect,
});

const questions = await prisma.question.findMany({
where: {
formId: checkinForm.id,
parentQuestionId: null,
},
select: {
id: true,
order: true,
inputType: {
select: {
name: true,
},
},
optionGroupId: true,
parentQuestionId: true,
},
orderBy: {
order: "asc",
},
});

const responseGroup = await prisma.responseGroup.create({
data: {},
});

await Promise.all(
questions.map((question) => {
populateQuestionResponses(question, teamMemberId, responseGroup.id);
}),
);

await prisma.formResponseCheckin.create({
data: {
voyageTeamMemberId: teamMember.id,
sprintId: teamMember.voyageTeam.voyage.sprints[0].id,
responseGroupId: responseGroup.id,
},
});
};
3 changes: 1 addition & 2 deletions prisma/seed/checklist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
import { prisma } from "./prisma-client";

export const populateChecklists = async () => {
await prisma.checklistItem.createMany({
Expand Down
4 changes: 1 addition & 3 deletions prisma/seed/forms/checkinform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
import { prisma } from "../prisma-client";

export const populateCheckinForm = async () => {
// Sprint - checkin form
Expand Down
4 changes: 1 addition & 3 deletions prisma/seed/forms/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { PrismaClient } from "@prisma/client";
import { populateCheckinForm } from "./checkinform";
import { populateSoloProjectForm } from "./solo-project";
import { populateVoyageApplicationForm } from "./voyage-app";

const prisma = new PrismaClient();
import { prisma } from "../prisma-client";

export const populateFormsAndResponses = async () => {
// test option choices for Voyage Application form
Expand Down
Loading
Loading