feat: add submission history
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Submission } from '../../entities/submission.entity';
|
||||
import { SubmissionAnswer } from '../../entities/submission-answer.entity';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { User } from '@/modules/users/entities/user.entity';
|
||||
import { AppResponse } from '@/common/responses';
|
||||
|
||||
@Injectable()
|
||||
export class HistoryAuthenticatedProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Submission Repository
|
||||
*/
|
||||
@InjectRepository(Submission)
|
||||
private readonly submissionRepository: Repository<Submission>,
|
||||
|
||||
/**
|
||||
* Inject Submission Answer Repository
|
||||
*/
|
||||
@InjectRepository(SubmissionAnswer)
|
||||
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
|
||||
) {}
|
||||
|
||||
public async getHistory(id: string, user: User) {
|
||||
let submission: Submission | null = null;
|
||||
|
||||
const submissionId = Number(id);
|
||||
|
||||
if (Number.isNaN(submissionId)) {
|
||||
throw new AppException(
|
||||
'validation.wrongFieldFormat',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
{ field: '$t(submissions.fields.submissionId)' },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
submission = await this.submissionRepository.findOne({
|
||||
where: {
|
||||
id: submissionId,
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
test: true,
|
||||
currentQuestion: {
|
||||
choices: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
|
||||
if (!submission) {
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{
|
||||
entity: '$t(submissions.fields.submission)',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let answers: SubmissionAnswer[] = [];
|
||||
|
||||
try {
|
||||
answers = await this.submissionAnswerRepository.find({
|
||||
where: {
|
||||
submission: {
|
||||
id: submission.id,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
question: {
|
||||
choices: true,
|
||||
},
|
||||
choice: true,
|
||||
choices: true,
|
||||
},
|
||||
order: {
|
||||
id: 'ASC',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
|
||||
const messages = answers.map((answer) => ({
|
||||
question: {
|
||||
id: answer.question.id,
|
||||
type: answer.question.type,
|
||||
content: answer.question.content,
|
||||
required: answer.question.required,
|
||||
metadata: answer.question.metadata,
|
||||
},
|
||||
answer: {
|
||||
choiceId: answer.choice?.id,
|
||||
choice: answer.choice
|
||||
? {
|
||||
id: answer.choice.id,
|
||||
text: answer.choice.text,
|
||||
order: answer.choice.order,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
choiceIds: answer.choices?.map((choice) => choice.id),
|
||||
|
||||
choices: answer.choices?.map((choice) => ({
|
||||
id: choice.id,
|
||||
text: choice.text,
|
||||
order: choice.order,
|
||||
})),
|
||||
|
||||
numericValue: answer.numericValue,
|
||||
},
|
||||
}));
|
||||
|
||||
return new AppResponse({
|
||||
messages,
|
||||
|
||||
currentQuestion: submission.currentQuestion
|
||||
? {
|
||||
id: submission.currentQuestion.id,
|
||||
type: submission.currentQuestion.type,
|
||||
content: submission.currentQuestion.content,
|
||||
required: submission.currentQuestion.required,
|
||||
metadata: submission.currentQuestion.metadata,
|
||||
choices: submission.currentQuestion.choices?.map((choice) => ({
|
||||
id: choice.id,
|
||||
text: choice.text,
|
||||
order: choice.order,
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
|
||||
isCompleted: submission.currentQuestion === null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { RedisService } from '@/common/modules/redis/providers/redis.service';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { AppResponse } from '@/common/responses';
|
||||
|
||||
import { Question } from '@/modules/questions/entities/question.entity';
|
||||
import { GuestSubmissionSession } from '../../interfaces/guest-submission-session.interface';
|
||||
import { isUUID } from 'class-validator';
|
||||
|
||||
@Injectable()
|
||||
export class HistoryGuestProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Redis Service
|
||||
*/
|
||||
private readonly redisService: RedisService,
|
||||
|
||||
/**
|
||||
* Inject Questions Repository
|
||||
*/
|
||||
@InjectRepository(Question)
|
||||
private readonly questionsRepository: Repository<Question>,
|
||||
) {}
|
||||
|
||||
public async getHistory(id: string) {
|
||||
if (!isUUID(id)) {
|
||||
throw new AppException(
|
||||
'validation.wrongFieldFormat',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
{ field: '$t(submissions.fields.submissionId)' },
|
||||
);
|
||||
}
|
||||
|
||||
const key = `guest_submission:${id}`;
|
||||
|
||||
let submission: GuestSubmissionSession | null = null;
|
||||
|
||||
try {
|
||||
submission = await this.redisService.get<GuestSubmissionSession>(key);
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
|
||||
if (!submission) {
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{
|
||||
entity: '$t(submissions.fields.submission)',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all question IDs that we need.
|
||||
*/
|
||||
const questionIds = new Set<number>();
|
||||
|
||||
for (const answer of submission.answers) {
|
||||
questionIds.add(answer.questionId);
|
||||
}
|
||||
|
||||
if (submission.currentQuestionId !== null) {
|
||||
questionIds.add(submission.currentQuestionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load questions from DB.
|
||||
*/
|
||||
let questions: Question[] = [];
|
||||
|
||||
if (questionIds.size > 0) {
|
||||
try {
|
||||
questions = await this.questionsRepository.find({
|
||||
where: {
|
||||
id: In([...questionIds]),
|
||||
},
|
||||
relations: {
|
||||
choices: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
const questionsMap = new Map<number, Question>();
|
||||
|
||||
for (const question of questions) {
|
||||
questionsMap.set(question.id, question);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build messages.
|
||||
*/
|
||||
const messages = submission.answers
|
||||
.map((answer) => {
|
||||
const question = questionsMap.get(answer.questionId);
|
||||
|
||||
if (!question) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedChoices = answer.choiceIds
|
||||
? question.choices.filter((choice) =>
|
||||
answer.choiceIds!.includes(choice.id),
|
||||
)
|
||||
: [];
|
||||
|
||||
const selectedChoice =
|
||||
answer.choiceId !== undefined
|
||||
? question.choices.find((choice) => choice.id === answer.choiceId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
question: {
|
||||
id: question.id,
|
||||
type: question.type,
|
||||
content: question.content,
|
||||
required: question.required,
|
||||
metadata: question.metadata,
|
||||
},
|
||||
|
||||
answer: {
|
||||
choiceId: answer.choiceId,
|
||||
|
||||
choice: selectedChoice
|
||||
? {
|
||||
id: selectedChoice.id,
|
||||
text: selectedChoice.text,
|
||||
order: selectedChoice.order,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
choiceIds: answer.choiceIds,
|
||||
|
||||
choices: selectedChoices.map((choice) => ({
|
||||
id: choice.id,
|
||||
text: choice.text,
|
||||
order: choice.order,
|
||||
})),
|
||||
|
||||
numericValue: answer.numericValue,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter((message) => message !== null);
|
||||
|
||||
/**
|
||||
* Current question.
|
||||
*/
|
||||
const currentQuestion =
|
||||
submission.currentQuestionId !== null
|
||||
? questionsMap.get(submission.currentQuestionId)
|
||||
: null;
|
||||
|
||||
return new AppResponse({
|
||||
messages,
|
||||
|
||||
currentQuestion: currentQuestion
|
||||
? {
|
||||
id: currentQuestion.id,
|
||||
type: currentQuestion.type,
|
||||
content: currentQuestion.content,
|
||||
required: currentQuestion.required,
|
||||
metadata: currentQuestion.metadata,
|
||||
|
||||
choices: currentQuestion.choices?.map((choice) => ({
|
||||
id: choice.id,
|
||||
text: choice.text,
|
||||
order: choice.order,
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
|
||||
isCompleted: submission.currentQuestionId === null,
|
||||
|
||||
result: submission.result
|
||||
? {
|
||||
score: submission.result.score,
|
||||
probability: submission.result.probability,
|
||||
title: submission.result.title,
|
||||
description: submission.result.description,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HistoryGuestProvider } from './history-guest.provider';
|
||||
import { HistoryAuthenticatedProvider } from './history-authenticated.provider';
|
||||
import { User } from '@/modules/users/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionHistoryProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject History Guest Provider
|
||||
*/
|
||||
private readonly historyGuestProvider: HistoryGuestProvider,
|
||||
|
||||
/**
|
||||
* Inject History Authenticated Provider
|
||||
*/
|
||||
private readonly historyAuthenticatedProvider: HistoryAuthenticatedProvider,
|
||||
) {}
|
||||
|
||||
public async getHistory(id: string, user?: User) {
|
||||
if (user) {
|
||||
return await this.historyAuthenticatedProvider.getHistory(id, user);
|
||||
} else {
|
||||
return await this.historyGuestProvider.getHistory(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { StartSubmissionProvider } from './start/start-submission.provider';
|
||||
import { User } from '@/modules/users/entities/user.entity';
|
||||
import { AnswerSubmissionProvider } from './answer/answer-submission.provider';
|
||||
import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto';
|
||||
import { SubmissionHistoryProvider } from './history/submission-history.provider';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionsService {
|
||||
@@ -16,6 +17,11 @@ export class SubmissionsService {
|
||||
* Inject Answer Submission Provider
|
||||
*/
|
||||
private readonly answerSubmissionProvider: AnswerSubmissionProvider,
|
||||
|
||||
/**
|
||||
* Inject Submission History Provider
|
||||
*/
|
||||
private readonly submissionHistoryProvider: SubmissionHistoryProvider,
|
||||
) {}
|
||||
|
||||
public async startSubmission(testId: number, user?: User) {
|
||||
@@ -33,4 +39,8 @@ export class SubmissionsService {
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
public async getSubmissionHistory(id: string, user?: User) {
|
||||
return await this.submissionHistoryProvider.getHistory(id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
@@ -53,4 +54,11 @@ export class SubmissionsController {
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':id/history')
|
||||
@UseGuards(OptionalJwtGuard)
|
||||
public async getHistory(@Param('id') id: string, @ActiveUser() user?: User) {
|
||||
return await this.submissionsService.getSubmissionHistory(id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import { CalculateResultProvider } from './providers/answer/calculate-result.pro
|
||||
import { ScoringModule } from '../scoring/scoring.module';
|
||||
import { AnswerGuestProvider } from './providers/answer/answer-guest.provider';
|
||||
import { AnswerAuthenticatedProvider } from './providers/answer/answer-authenticated.provider';
|
||||
import { HistoryAuthenticatedProvider } from './providers/history/history-authenticated.provider';
|
||||
import { HistoryGuestProvider } from './providers/history/history-guest.provider';
|
||||
import { SubmissionHistoryProvider } from './providers/history/submission-history.provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,6 +33,9 @@ import { AnswerAuthenticatedProvider } from './providers/answer/answer-authentic
|
||||
AnswerAuthenticatedProvider,
|
||||
AnswerGuestProvider,
|
||||
AnswerSubmissionProvider,
|
||||
HistoryAuthenticatedProvider,
|
||||
HistoryGuestProvider,
|
||||
SubmissionHistoryProvider,
|
||||
SubmissionsService,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user