wip
This commit is contained in:
Generated
+40
@@ -15,6 +15,8 @@
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"pg": "^8.16.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -3562,6 +3564,12 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.1.tgz",
|
||||
"integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
@@ -5508,6 +5516,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
||||
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.11.8",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -8657,6 +8682,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.8",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz",
|
||||
"integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -11918,6 +11949,15 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"pg": "^8.16.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { Choice } from './polls/entities/choice.entity';
|
||||
import { InviteCode } from './polls/entities/inviteCode.entity';
|
||||
import { Poll } from './polls/entities/poll.entity';
|
||||
import { Question } from './polls/entities/question.entity';
|
||||
import { QuestionOption } from './polls/entities/questionOptions';
|
||||
import { QuestionOption } from './polls/entities/questionOptions.entity';
|
||||
import { User } from './polls/entities/user.entity';
|
||||
|
||||
void ConfigModule.forRoot({
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Vote au jugement majoritaire')
|
||||
.setDescription('API description')
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose } from 'class-transformer';
|
||||
import { IsNumber, IsString } from 'class-validator';
|
||||
|
||||
export class AnswerCreateDto {
|
||||
@Expose()
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
questionId: number;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
choice: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose } from 'class-transformer';
|
||||
|
||||
export class ChoiceReadDto {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
value: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
order: string;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
import { QuestionUpdateDto } from './question.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class PollUpdateDTO {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
questions: QuestionUpdateDto[];
|
||||
|
||||
@Expose()
|
||||
@IsUUID()
|
||||
@ApiProperty()
|
||||
publicCode: string;
|
||||
}
|
||||
|
||||
export class PollReadDTO extends PollUpdateDTO {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
published: boolean;
|
||||
}
|
||||
|
||||
export class PollReadAdminDTO extends PollReadDTO {
|
||||
@Expose()
|
||||
@IsUUID()
|
||||
@ApiProperty()
|
||||
editCode: string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
import { ChoiceReadDto } from './choice.dto';
|
||||
|
||||
export class QuestionUpdateDto {
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@Expose()
|
||||
value: string;
|
||||
|
||||
@Expose()
|
||||
mandatory: boolean;
|
||||
|
||||
@Expose()
|
||||
order: number;
|
||||
|
||||
@Expose()
|
||||
choices: ChoiceReadDto[];
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Question } from './question.entity';
|
||||
import { User } from './user.entity';
|
||||
import { QuestionOption } from './questionOptions';
|
||||
import { QuestionOption } from './questionOptions.entity';
|
||||
import { Choice } from './choice.entity';
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -18,7 +18,7 @@ export class InviteCode {
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
max_usages: number;
|
||||
maxUsages: number;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
|
||||
@@ -15,12 +15,16 @@ export class Poll {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
@Column({ default: 'Sondage' })
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
publicCode: string;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
editCode: string;
|
||||
|
||||
@@ -4,12 +4,16 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
Unique,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { Poll } from './poll.entity';
|
||||
import { Answer } from './answer.entity';
|
||||
import { QuestionOption } from './questionOptions';
|
||||
import { Choice } from './choice.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['poll', 'order'])
|
||||
export class Question {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
@@ -26,8 +30,23 @@ export class Question {
|
||||
@ManyToOne(() => Poll, (poll: Poll) => poll.questions)
|
||||
poll: Poll;
|
||||
|
||||
@OneToMany(() => QuestionOption, (questionOption) => questionOption.question)
|
||||
options: QuestionOption[];
|
||||
// @OneToMany(() => QuestionOption, (questionOption) => questionOption.question)
|
||||
// options: QuestionOption[];
|
||||
|
||||
@ManyToMany(() => Choice, { eager: true })
|
||||
@JoinTable({
|
||||
name: 'question_options',
|
||||
joinColumn: {
|
||||
name: 'question',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: 'choice',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
synchronize: false,
|
||||
})
|
||||
choices: Choice[];
|
||||
|
||||
@OneToMany(() => Answer, (answer) => answer.question)
|
||||
answers: Answer[];
|
||||
|
||||
+1
-1
@@ -11,6 +11,6 @@ export class QuestionOption {
|
||||
@ManyToOne(() => Choice)
|
||||
choice: Choice;
|
||||
|
||||
@ManyToOne(() => Question, (question) => question.options)
|
||||
@ManyToOne(() => Question)
|
||||
question: Question;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollReadAdminDTO, PollReadDTO, PollUpdateDTO } from './dto/poll.dto';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
@ApiTags('polls')
|
||||
@Controller('polls')
|
||||
export class PollController {
|
||||
constructor(private readonly pollService: PollsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create poll' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Poll successfully created.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async create(): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.create();
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
|
||||
@Post(':publicCode/:editCode')
|
||||
@ApiOperation({ summary: 'Update a poll' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Poll successfully created.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async update(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('editCode', new ParseUUIDPipe()) editCode: string,
|
||||
@Body() pollUpdate: PollUpdateDTO,
|
||||
): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.readAdmin(publicCode, editCode);
|
||||
if (!poll) {
|
||||
throw new HttpException(
|
||||
'Poll not found or invalid editCode',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
await this.pollService.update(poll, pollUpdate);
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
|
||||
@Get(':publicCode')
|
||||
@ApiOperation({ summary: 'Get public information about a poll' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Found',
|
||||
type: PollReadDTO,
|
||||
})
|
||||
async get(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
): Promise<PollReadDTO> {
|
||||
const poll = await this.pollService.read(publicCode);
|
||||
if (!poll) {
|
||||
throw new HttpException('Poll not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!poll?.published) {
|
||||
throw new HttpException('Poll not yet published', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return plainToInstance(PollReadDTO, poll);
|
||||
}
|
||||
|
||||
@Get(':publicCode/:editCode')
|
||||
@ApiOperation({ summary: 'Get admin information about a poll' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Found',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async getAdmin(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('editCode', new ParseUUIDPipe()) editCode: string,
|
||||
): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.readAdmin(publicCode, editCode);
|
||||
if (!poll) {
|
||||
throw new HttpException(
|
||||
'Poll not found or invalid editCode',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollController } from './polls.controller';
|
||||
import { InviteCode } from './entities/inviteCode.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Poll])],
|
||||
imports: [TypeOrmModule.forFeature([Poll, InviteCode])],
|
||||
controllers: [PollController],
|
||||
providers: [PollsService],
|
||||
})
|
||||
export class PollsModule {}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PollsService } from './polls.service';
|
||||
|
||||
describe('PollsService', () => {
|
||||
let service: PollsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PollsService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PollsService>(PollsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PollUpdateDTO } from './dto/poll.dto';
|
||||
import { InviteCode } from './entities/inviteCode.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PollsService {
|
||||
constructor(
|
||||
@InjectRepository(Poll) private pollRepository: Repository<Poll>,
|
||||
@InjectRepository(InviteCode)
|
||||
private inviteRepository: Repository<InviteCode>,
|
||||
) {}
|
||||
|
||||
async create() {
|
||||
const poll = this.pollRepository.create();
|
||||
await this.pollRepository.save(poll);
|
||||
return poll;
|
||||
}
|
||||
|
||||
async update(poll: Poll, pollUpdate: PollUpdateDTO) {
|
||||
if (poll.published) {
|
||||
throw Error('This poll is closed !');
|
||||
}
|
||||
await this.pollRepository.update(poll, pollUpdate);
|
||||
}
|
||||
|
||||
async read(pollId: string) {
|
||||
const poll = await this.pollRepository.findOneBy({ publicCode: pollId });
|
||||
return poll;
|
||||
}
|
||||
|
||||
async readAdmin(pollId: string, editCode: string) {
|
||||
const poll = await this.pollRepository.findOneBy({
|
||||
publicCode: pollId,
|
||||
editCode: editCode,
|
||||
});
|
||||
return poll;
|
||||
}
|
||||
|
||||
async hasAccess(poll: Poll, accessCode: string) {
|
||||
return await this.inviteRepository.exists({
|
||||
where: {
|
||||
poll: { id: poll.id },
|
||||
value: accessCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollReadAdminDTO } from './dto/poll.dto';
|
||||
import { QuestionsService } from './questions.service';
|
||||
import { AnswerCreateDto } from './dto/answer.dto';
|
||||
|
||||
@ApiTags('questions')
|
||||
@Controller('questions')
|
||||
export class QuestionsController {
|
||||
constructor(
|
||||
private readonly questionsService: QuestionsService,
|
||||
private readonly pollService: PollsService,
|
||||
) {}
|
||||
|
||||
@Post(':publicCode/:accessCode')
|
||||
@ApiOperation({ summary: 'Vote for a question' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Vote successfully saved.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async vote(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('accessCode', new ParseUUIDPipe()) accessCode: string,
|
||||
@Body() answer: AnswerCreateDto,
|
||||
) {
|
||||
const poll = await this.pollService.read(publicCode);
|
||||
if (!poll) {
|
||||
throw new HttpException('Poll not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!poll?.published) {
|
||||
throw new HttpException('Poll not yet published', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (!(await this.pollService.hasAccess(poll, accessCode))) {
|
||||
throw new HttpException('Invalid access code', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
const question = this.questionsService.get(poll, answer.questionId);
|
||||
if (!question) {
|
||||
throw new HttpException('Question not found', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
await this.questionsService.vote(question, null, answer.choice);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { Question } from './entities/question.entity';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsService {
|
||||
constructor(
|
||||
@InjectRepository(Question)
|
||||
private questionRepository: Repository<Question>,
|
||||
) {}
|
||||
|
||||
get(poll: Poll, questionId: number) {
|
||||
const question = poll.questions.find((q) => q.id == questionId);
|
||||
return question;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user