feat: complete publish test logic
This commit is contained in:
@@ -9,7 +9,12 @@
|
||||
},
|
||||
"errors": {
|
||||
"intPrice": "Price should be an integer.",
|
||||
"priceAccessConstraint": "Test's price depends on access type of 'Paid'."
|
||||
"priceAccessConstraint": "Test's price depends on access type of 'Paid'.",
|
||||
"mustHaveSingleStart": "Test must have single start.",
|
||||
"hasOrphanQuestions": "Test has some orphan questions.",
|
||||
"hasCycle": "Test has cycle.",
|
||||
"notEndingFlow": "Test include some flows which never end.",
|
||||
"notActiveTest": "Test is not active."
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "Test Created Successfully."
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
},
|
||||
"errors": {
|
||||
"intPrice": "قیمت باید عدد صحیح باشد.",
|
||||
"priceAccessConstraint": "قیمت تست به نوع دسترسی 'پرداخت' وابسته است."
|
||||
"priceAccessConstraint": "قیمت تست به نوع دسترسی 'پرداخت' وابسته است.",
|
||||
"mustHaveSingleStart": "تست باید یک سوال شروع داشته باشد.",
|
||||
"hasOrphanQuestions": "تست شامل تعدادی سوال یتیم است.",
|
||||
"hasCycle": "تست شامل حلقه است.",
|
||||
"notEndingFlow": "تست شامل جریان بی پایان است.",
|
||||
"notActiveTest": "تست مورد نظر فعال نیست."
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "تست با موفقیت ایجاد شد."
|
||||
|
||||
@@ -16,6 +16,8 @@ import { Test } from '@/modules/tests/entities/test.entity';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { RedisService } from '@/common/modules/redis/providers/redis.service';
|
||||
import { GuestSubmissionSession } from '../interfaces/guest-submission-session.interfaces';
|
||||
import { TestStatus } from '@/modules/tests/enums/test-status.enum';
|
||||
import { QuestionTransitionService } from '@/modules/questions/providers/transition/question-transition.service';
|
||||
|
||||
@Injectable()
|
||||
export class StartSubmissionProvider {
|
||||
@@ -35,6 +37,11 @@ export class StartSubmissionProvider {
|
||||
* Inject Redis Service
|
||||
*/
|
||||
private readonly redisService: RedisService,
|
||||
|
||||
/**
|
||||
* Inject Transitions Service
|
||||
*/
|
||||
private readonly transitionsService: QuestionTransitionService,
|
||||
) {}
|
||||
|
||||
public async start(testId: number, user?: User) {
|
||||
@@ -69,6 +76,10 @@ export class StartSubmissionProvider {
|
||||
},
|
||||
});
|
||||
|
||||
if (!test.isActive || test.status !== TestStatus.PUBLISHED) {
|
||||
throw new ForbiddenException('tests.errors.notActiveTest');
|
||||
}
|
||||
|
||||
if (test.accessType !== AccessType.FREE && !user) {
|
||||
throw new ForbiddenException('auth.errors.accessDenied');
|
||||
}
|
||||
@@ -115,6 +126,8 @@ export class StartSubmissionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// const startTransition = await this.transitionsService.
|
||||
|
||||
return new AppResponse({
|
||||
submissionId: submission.id,
|
||||
question: test.questions.length > 0 ? test.questions[0] : null,
|
||||
|
||||
@@ -6,12 +6,10 @@ import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { QuestionTransition } from '@/modules/questions/entities/question-transition.entity';
|
||||
import { Question } from '@/modules/questions/entities/question.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PublishTestValidatorProvider {
|
||||
private readonly graph = new Map<number, QuestionTransition[]>();
|
||||
private readonly memo = new Map<number, boolean>();
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* Inject Tests Repository
|
||||
@@ -29,6 +27,7 @@ export class PublishTestValidatorProvider {
|
||||
id: testId,
|
||||
},
|
||||
relations: {
|
||||
questions: true,
|
||||
transitions: {
|
||||
fromQuestion: true,
|
||||
toQuestion: true,
|
||||
@@ -62,16 +61,69 @@ export class PublishTestValidatorProvider {
|
||||
|
||||
const startQuestionId = startTransitions[0].toQuestion!.id;
|
||||
|
||||
const transitionsGraph = new Map<number, QuestionTransition[]>();
|
||||
|
||||
this.createTransitionsGraph(transitions, transitionsGraph);
|
||||
|
||||
if (
|
||||
this.hasOrphanQuestions(startQuestionId, transitionsGraph, test.questions)
|
||||
) {
|
||||
throw new AppException(
|
||||
'tests.errors.hasOrphanQuestions',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const visiting = new Set<number>();
|
||||
const visitedCycle = new Set<number>();
|
||||
|
||||
if (
|
||||
this.graphHasCycle(
|
||||
startQuestionId,
|
||||
visiting,
|
||||
visitedCycle,
|
||||
transitionsGraph,
|
||||
)
|
||||
) {
|
||||
throw new AppException('tests.errors.hasCycle', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const endingQuestionsMemo = new Map<number, boolean>();
|
||||
|
||||
if (
|
||||
!this.allPathsReachEnd(
|
||||
startQuestionId,
|
||||
endingQuestionsMemo,
|
||||
transitionsGraph,
|
||||
)
|
||||
) {
|
||||
throw new AppException(
|
||||
'tests.errors.notEndingFlow',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createTransitionsGraph(
|
||||
transitions: QuestionTransition[],
|
||||
graph: Map<number, QuestionTransition[]>,
|
||||
) {
|
||||
for (const transition of transitions) {
|
||||
if (!transition.fromQuestion) continue;
|
||||
|
||||
const list = this.graph.get(transition.fromQuestion.id) ?? [];
|
||||
const list = graph.get(transition.fromQuestion.id) ?? [];
|
||||
|
||||
list.push(transition);
|
||||
|
||||
this.graph.set(transition.fromQuestion.id, list);
|
||||
graph.set(transition.fromQuestion.id, list);
|
||||
}
|
||||
}
|
||||
|
||||
private hasOrphanQuestions(
|
||||
startQuestionId: number,
|
||||
graph: Map<number, QuestionTransition[]>,
|
||||
allQuestions: Array<Question>,
|
||||
) {
|
||||
const visited = new Set<number>();
|
||||
const stack = [startQuestionId];
|
||||
|
||||
@@ -83,9 +135,9 @@ export class PublishTestValidatorProvider {
|
||||
|
||||
visited.add(id);
|
||||
|
||||
const next = this.graph.get(id) ?? [];
|
||||
const nextTransitions = graph.get(id) ?? [];
|
||||
|
||||
for (const transition of next) {
|
||||
for (const transition of nextTransitions) {
|
||||
const toQuestion = transition.toQuestion;
|
||||
|
||||
if (toQuestion) {
|
||||
@@ -94,35 +146,33 @@ export class PublishTestValidatorProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const orphanQuestions = test.questions.filter((q) => !visited.has(q.id));
|
||||
const orphanQuestions = allQuestions.filter((q) => !visited.has(q.id));
|
||||
|
||||
if (orphanQuestions.length > 0) {
|
||||
throw new AppException(
|
||||
'questions.errors.test.hasOrphanQuestions',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
return orphanQuestions.length > 0;
|
||||
}
|
||||
|
||||
if (!this.questionReachesEnd(startQuestionId)) {
|
||||
throw new AppException(
|
||||
'questions.errors.test.noPathToEnd',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const visiting = new Set<number>();
|
||||
const visitedCycle = new Set<number>();
|
||||
|
||||
// TODO: Fix this
|
||||
const hasCycle = (id: number): boolean => {
|
||||
private graphHasCycle(
|
||||
id: number,
|
||||
visiting: Set<number>,
|
||||
visitedCycle: Set<number>,
|
||||
graph: Map<number, QuestionTransition[]>,
|
||||
): boolean {
|
||||
if (visiting.has(id)) return true;
|
||||
if (visitedCycle.has(id)) return false;
|
||||
|
||||
visiting.add(id);
|
||||
|
||||
for (const transition of this.graph.get(id) ?? []) {
|
||||
for (const transition of graph.get(id) ?? []) {
|
||||
if (transition.toQuestion) {
|
||||
if (hasCycle(transition.toQuestion.id)) return true;
|
||||
if (
|
||||
this.graphHasCycle(
|
||||
transition.toQuestion.id,
|
||||
visiting,
|
||||
visitedCycle,
|
||||
graph,
|
||||
)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,37 +180,51 @@ export class PublishTestValidatorProvider {
|
||||
visitedCycle.add(id);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (hasCycle(startQuestionId)) {
|
||||
throw new AppException(
|
||||
'questions.errors.test.hasCycle',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private questionReachesEnd(questionId: number) {
|
||||
if (this.memo.has(questionId)) return this.memo.get(questionId)!;
|
||||
private allPathsReachEnd(
|
||||
questionId: number,
|
||||
memo: Map<number, boolean>,
|
||||
graph: Map<number, QuestionTransition[]>,
|
||||
): boolean {
|
||||
if (memo.has(questionId)) {
|
||||
return memo.get(questionId)!;
|
||||
}
|
||||
|
||||
const outgoing = this.graph.get(questionId) ?? [];
|
||||
const outgoing = graph.get(questionId) ?? [];
|
||||
|
||||
if (outgoing.length === 0) {
|
||||
memo.set(questionId, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
let hasEndTransition = false;
|
||||
|
||||
for (const transition of outgoing) {
|
||||
if (transition.type === QuestionTransitionType.END) {
|
||||
this.memo.set(questionId, true);
|
||||
return true;
|
||||
hasEndTransition = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
transition.toQuestion &&
|
||||
this.questionReachesEnd(transition.toQuestion.id)
|
||||
!transition.toQuestion ||
|
||||
!this.allPathsReachEnd(transition.toQuestion.id, memo, graph)
|
||||
) {
|
||||
this.memo.set(questionId, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.memo.set(questionId, false);
|
||||
memo.set(questionId, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const result =
|
||||
hasEndTransition ||
|
||||
outgoing.every(
|
||||
(t) =>
|
||||
t.type === QuestionTransitionType.END ||
|
||||
(t.toQuestion && memo.get(t.toQuestion.id) === true),
|
||||
);
|
||||
|
||||
memo.set(questionId, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { AppResponse } from '@/common/responses';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { UpdateTestDTO } from '../dtos/update-test.dto';
|
||||
import { AccessType } from '../enums/access-types.enum';
|
||||
import { PublishTestValidatorProvider } from './publish-test-validator.provider';
|
||||
import { TestStatus } from '../enums/test-status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class TestsCommandProvider {
|
||||
@@ -23,6 +25,11 @@ export class TestsCommandProvider {
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
|
||||
/**
|
||||
* Inject Publish Test Validator Provider
|
||||
*/
|
||||
private readonly publishTestValidatorProvider: PublishTestValidatorProvider,
|
||||
) {}
|
||||
|
||||
public async createOne(createTestDto: CreateTestDTO) {
|
||||
@@ -76,6 +83,10 @@ export class TestsCommandProvider {
|
||||
|
||||
Object.assign(test, updateTestDto);
|
||||
|
||||
if (updateTestDto.status === TestStatus.PUBLISHED) {
|
||||
await this.publishTestValidatorProvider.validate(test.id);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.testRepository.save(test);
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { filterTests } from '../helpers/filter-tests.helper';
|
||||
import Utils from '@/common/utils';
|
||||
import { Test } from '../entities/test.entity';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { TestStatus } from '../enums/test-status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class TestsQueryProvider {
|
||||
@@ -49,7 +50,7 @@ export class TestsQueryProvider {
|
||||
public async findAllForUser() {
|
||||
try {
|
||||
return await this.testRepository.find({
|
||||
where: { isActive: true },
|
||||
where: { isActive: true, status: TestStatus.PUBLISHED },
|
||||
select: {
|
||||
id: true,
|
||||
accessType: true,
|
||||
|
||||
@@ -5,11 +5,17 @@ import { TestsService } from './providers/tests.service';
|
||||
import { TestsController } from './tests.controller';
|
||||
import { TestsQueryProvider } from './providers/tests-query.provider';
|
||||
import { TestsCommandProvider } from './providers/tests-command.provider';
|
||||
import { PublishTestValidatorProvider } from './providers/publish-test-validator.provider';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Test])],
|
||||
controllers: [TestsController],
|
||||
providers: [TestsService, TestsQueryProvider, TestsCommandProvider],
|
||||
providers: [
|
||||
TestsService,
|
||||
TestsQueryProvider,
|
||||
PublishTestValidatorProvider,
|
||||
TestsCommandProvider,
|
||||
],
|
||||
exports: [TestsService],
|
||||
})
|
||||
export class TestsModule {}
|
||||
|
||||
Reference in New Issue
Block a user