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

Feature: ideation/projectIdea selection #136

Merged
merged 10 commits into from
Apr 26, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Another example [here](https://co-pilot.dev/changelog)
- Add voyage project submission form seed ([#131](https://github.com/chingu-x/chingu-dashboard-be/pull/131))
- Add voyage project submission controller, service, e2e tests, responses seed ([#133](https://github.com/chingu-x/chingu-dashboard-be/pull/133))


- Add new endpoints to select/reset team project ideation ([#136](https://github.com/chingu-x/chingu-dashboard-be/pull/136))

### Changed

- Update docker compose and scripts in package.json to include a test database container and remove usage of .env.dev to avoid confusion ([#100](https://github.com/chingu-x/chingu-dashboard-be/pull/100))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ProjectIdea" ADD COLUMN "isSelected" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ model ProjectIdea {
title String
description String
vision String
isSelected Boolean @default(false)

createdAt DateTime @default(now()) @db.Timestamptz()
updatedAt DateTime @updatedAt
Expand Down
3 changes: 3 additions & 0 deletions src/ideations/entities/ideation.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class Ideation implements ProjectIdea {
})
vision: string;

@ApiProperty({ example: "false" })
isSelected: boolean;

@ApiProperty({ example: "2024-01-08T00:00:00.000Z" })
createdAt: Date;

Expand Down
91 changes: 90 additions & 1 deletion src/ideations/ideations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { IdeationsService } from "./ideations.service";
import { CreateIdeationDto } from "./dto/create-ideation.dto";
import { UpdateIdeationDto } from "./dto/update-ideation.dto";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { ApiOperation, ApiResponse, ApiTags, ApiParam } from "@nestjs/swagger";
import {
BadRequestErrorResponse,
ConflictErrorResponse,
Expand All @@ -28,6 +28,8 @@ import {
import { AppPermissions } from "../auth/auth.permissions";
import { Permissions } from "../global/decorators/permissions.decorator";
import { CustomRequest } from "../global/types/CustomRequest";
import { Roles } from "../global/decorators/roles.decorator";
import { AppRoles } from "../auth/auth.roles";

@Controller()
@ApiTags("Voyage - Ideations")
Expand Down Expand Up @@ -237,4 +239,91 @@ export class IdeationsController {
ideationId,
);
}

@ApiOperation({
summary: "Selects one ideation as team project for voyage.",
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: "User is not authorized to perform this action.",
type: UnauthorizedErrorResponse,
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: "Ideation with given ID does not exist.",
type: NotFoundErrorResponse,
})
@ApiResponse({
status: HttpStatus.CONFLICT,
description: "An ideation has already been selected.",
type: ConflictErrorResponse,
})
@ApiResponse({
status: HttpStatus.CREATED,
description: "Successfully selected ideation.",
type: IdeationVoteResponse,
})
@ApiParam({
name: "teamId",
description: "voyage team Id",
type: "Integer",
required: true,
example: 1,
})
@ApiParam({
name: "ideationId",
description: "ideation Id",
type: "Integer",
required: true,
example: 1,
})
@Permissions(AppPermissions.OWN_TEAM)
@Post("/:ideationId/select")
setIdeationSelection(
@Request() req: CustomRequest,
@Param("teamId", ParseIntPipe) teamId: number,
@Param("ideationId", ParseIntPipe) ideationId: number,
) {
return this.ideationsService.setIdeationSelection(
req,
teamId,
ideationId,
);
}

@ApiOperation({
summary: "Clears the current ideation selection for team.",
description: "Admin only allowed.",
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: "User is not authorized to perform this action.",
type: UnauthorizedErrorResponse,
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: "Ideation for this team does not exist.",
type: NotFoundErrorResponse,
})
@ApiResponse({
status: HttpStatus.CREATED,
description: "Successfully cleared ideation selection.",
type: IdeationVoteResponse,
})
@ApiParam({
name: "teamId",
description: "voyage team Id",
type: "Integer",
required: true,
example: 1,
})
@Permissions(AppPermissions.OWN_TEAM)
@Roles(AppRoles.Admin)
@Post("/reset-selection")
resetIdeationSelection(
@Request() req: CustomRequest,
@Param("teamId", ParseIntPipe) teamId: number,
) {
return this.ideationsService.resetIdeationSelection(req, teamId);
}
}
73 changes: 73 additions & 0 deletions src/ideations/ideations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export class IdeationsService {
title: true,
description: true,
vision: true,
isSelected: true,
createdAt: true,
updatedAt: true,
contributedBy: {
Expand Down Expand Up @@ -258,6 +259,78 @@ export class IdeationsService {
}
}

async getSelectedIdeation(teamId: number) {
//get all team member ids
const teamMemberIds = await this.prisma.voyageTeamMember.findMany({
where: {
voyageTeamId: teamId,
},
select: {
id: true,
},
});
//extract ids into array
const idArray = teamMemberIds.map((member) => member.id);
//search all team member project ideas for isSelected === true
return await this.prisma.projectIdea.findFirst({
where: {
voyageTeamMemberId: {
in: idArray,
},
isSelected: true,
},
});
}

async setIdeationSelection(
req: CustomRequest,
teamId: number,
ideationId: number,
) {
try {
const currentSelection = await this.getSelectedIdeation(teamId);
if (currentSelection) {
throw new ConflictException(
`Ideation already selected for team ${teamId}`,
);
}
return await this.prisma.projectIdea.update({
where: {
id: ideationId,
},
data: {
isSelected: true,
},
});
} catch (e) {
if (e.code === "P2025") {
throw new NotFoundException(e.meta.cause);
}
throw e;
}
}

async resetIdeationSelection(req: CustomRequest, teamId: number) {
try {
//find current selection, if any
const selection = await this.getSelectedIdeation(teamId);
if (selection) {
return await this.prisma.projectIdea.update({
where: {
id: selection.id,
},
data: {
isSelected: false,
},
});
}
} catch (e) {
throw e;
}
//default
throw new NotFoundException(`no ideation found for team ${teamId}`);
}

// TODO: this function seems to be unused but might be useful for making new permission guard
private async getTeamMemberIdByIdeation(ideationId: number) {
const voyageTeamMemberId = await this.prisma.projectIdea.findFirst({
Expand Down
Loading
Loading