feat: add user submission history preparations

This commit is contained in:
2026-09-08 17:44:47 +03:30
parent f90b802419
commit f0bf186b6c
15 changed files with 82 additions and 82 deletions
+21
View File
@@ -7,3 +7,24 @@ export const normalizePhone = (phone: string): string => {
return phone; return phone;
}; };
type GenerateCodeOptions = {
prefix?: string;
length?: number;
};
export const generateCode = (options?: GenerateCodeOptions) => {
const prefix = options?.prefix || '';
const length = options?.length || 5;
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let random = '';
for (let i = 0; i < length; i++) {
random += characters.charAt(Math.floor(Math.random() * characters.length));
}
return `${prefix}${random}`;
};
-24
View File
@@ -261,7 +261,6 @@ async function seedTest() {
const q1Yes = choiceRepository.create({ const q1Yes = choiceRepository.create({
question: q1, question: q1,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 1, score: 1,
}); });
@@ -269,7 +268,6 @@ async function seedTest() {
const q1No = choiceRepository.create({ const q1No = choiceRepository.create({
question: q1, question: q1,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -285,7 +283,6 @@ async function seedTest() {
const q2Yes = choiceRepository.create({ const q2Yes = choiceRepository.create({
question: q2, question: q2,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 1, score: 1,
}); });
@@ -293,7 +290,6 @@ async function seedTest() {
const q2No = choiceRepository.create({ const q2No = choiceRepository.create({
question: q2, question: q2,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -301,7 +297,6 @@ async function seedTest() {
const q2NotSure = choiceRepository.create({ const q2NotSure = choiceRepository.create({
question: q2, question: q2,
text: 'مطمئن نیستم', text: 'مطمئن نیستم',
value: 2,
order: 3, order: 3,
score: 1, score: 1,
}); });
@@ -332,7 +327,6 @@ async function seedTest() {
choiceRepository.create({ choiceRepository.create({
question: q3, question: q3,
text, text,
value: index + 1,
order: index + 1, order: index + 1,
score: 2, score: 2,
}), }),
@@ -363,7 +357,6 @@ async function seedTest() {
choiceRepository.create({ choiceRepository.create({
question: q4, question: q4,
text, text,
value: index + 1,
order: index + 1, order: index + 1,
score: 1, score: 1,
}), }),
@@ -380,7 +373,6 @@ async function seedTest() {
const q5Yes = choiceRepository.create({ const q5Yes = choiceRepository.create({
question: q5, question: q5,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 1, score: 1,
}); });
@@ -388,7 +380,6 @@ async function seedTest() {
const q5No = choiceRepository.create({ const q5No = choiceRepository.create({
question: q5, question: q5,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -396,7 +387,6 @@ async function seedTest() {
const q5NotSure = choiceRepository.create({ const q5NotSure = choiceRepository.create({
question: q5, question: q5,
text: 'مطمئن نیستم', text: 'مطمئن نیستم',
value: 2,
order: 3, order: 3,
score: 1, score: 1,
}); });
@@ -412,7 +402,6 @@ async function seedTest() {
const q6Yes = choiceRepository.create({ const q6Yes = choiceRepository.create({
question: q6, question: q6,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 1, score: 1,
}); });
@@ -420,7 +409,6 @@ async function seedTest() {
const q6No = choiceRepository.create({ const q6No = choiceRepository.create({
question: q6, question: q6,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -428,7 +416,6 @@ async function seedTest() {
const q6NotSure = choiceRepository.create({ const q6NotSure = choiceRepository.create({
question: q6, question: q6,
text: 'مطمئن نیستم', text: 'مطمئن نیستم',
value: 2,
order: 3, order: 3,
score: 1, score: 1,
}); });
@@ -444,7 +431,6 @@ async function seedTest() {
const q7Yes = choiceRepository.create({ const q7Yes = choiceRepository.create({
question: q7, question: q7,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 2, score: 2,
}); });
@@ -452,7 +438,6 @@ async function seedTest() {
const q7No = choiceRepository.create({ const q7No = choiceRepository.create({
question: q7, question: q7,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -468,7 +453,6 @@ async function seedTest() {
const q8Yes = choiceRepository.create({ const q8Yes = choiceRepository.create({
question: q8, question: q8,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 2, score: 2,
}); });
@@ -476,7 +460,6 @@ async function seedTest() {
const q8No = choiceRepository.create({ const q8No = choiceRepository.create({
question: q8, question: q8,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -492,7 +475,6 @@ async function seedTest() {
const q10Yes = choiceRepository.create({ const q10Yes = choiceRepository.create({
question: q10, question: q10,
text: 'بله', text: 'بله',
value: 1,
order: 1, order: 1,
score: 1, score: 1,
}); });
@@ -500,7 +482,6 @@ async function seedTest() {
const q10No = choiceRepository.create({ const q10No = choiceRepository.create({
question: q10, question: q10,
text: 'خیر', text: 'خیر',
value: 0,
order: 2, order: 2,
score: 0, score: 0,
}); });
@@ -516,7 +497,6 @@ async function seedTest() {
const q12Always = choiceRepository.create({ const q12Always = choiceRepository.create({
question: q12, question: q12,
text: 'همیشه با همهٔ شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.', text: 'همیشه با همهٔ شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.',
value: 1,
order: 1, order: 1,
score: 0, score: 0,
}); });
@@ -524,7 +504,6 @@ async function seedTest() {
const q12Sometimes = choiceRepository.create({ const q12Sometimes = choiceRepository.create({
question: q12, question: q12,
text: 'گاهی با شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.', text: 'گاهی با شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.',
value: 2,
order: 2, order: 2,
score: 2, score: 2,
}); });
@@ -532,7 +511,6 @@ async function seedTest() {
const q12Never = choiceRepository.create({ const q12Never = choiceRepository.create({
question: q12, question: q12,
text: 'هیچ‌وقت با هیچ‌کدام از شرکای جنسی‌ام از کاندوم استفاده نکرده‌ام.', text: 'هیچ‌وقت با هیچ‌کدام از شرکای جنسی‌ام از کاندوم استفاده نکرده‌ام.',
value: 3,
order: 3, order: 3,
score: 5, score: 5,
}); });
@@ -540,7 +518,6 @@ async function seedTest() {
const q12NotApplicable = choiceRepository.create({ const q12NotApplicable = choiceRepository.create({
question: q12, question: q12,
text: 'سؤال در مورد من صحت ندارد؛ چون که رابطهٔ واژنی یا مقعدی نداشته‌ام.', text: 'سؤال در مورد من صحت ندارد؛ چون که رابطهٔ واژنی یا مقعدی نداشته‌ام.',
value: 4,
order: 4, order: 4,
score: 1, score: 1,
}); });
@@ -548,7 +525,6 @@ async function seedTest() {
const q12NotSure = choiceRepository.create({ const q12NotSure = choiceRepository.create({
question: q12, question: q12,
text: 'مطمئن نیستم.', text: 'مطمئن نیستم.',
value: 5,
order: 5, order: 5,
score: 2, score: 2,
}); });
-1
View File
@@ -9,7 +9,6 @@
"question": "Question", "question": "Question",
"text": "Text", "text": "Text",
"value": "Value", "value": "Value",
"choice": "Choice",
"score": "Score", "score": "Score",
"weight": "Weight", "weight": "Weight",
"rangeRule": "Range Rule", "rangeRule": "Range Rule",
-1
View File
@@ -8,7 +8,6 @@
"metadata": "تنظیمات", "metadata": "تنظیمات",
"question": "سوال", "question": "سوال",
"text": "متن", "text": "متن",
"value": "مقدار",
"choice": "گزینه", "choice": "گزینه",
"score": "نمره", "score": "نمره",
"weight": "وزن", "weight": "وزن",
@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class PrepUserSubmissionHistory1788876455659 implements MigrationInterface {
name = 'PrepUserSubmissionHistory1788876455659';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."unq_question_multi_choice"`);
await queryRunner.query(`ALTER TABLE "choice" DROP COLUMN "value"`);
await queryRunner.query(
`ALTER TABLE "submission_answer" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`,
);
await queryRunner.query(
`ALTER TABLE "submission" ADD "code" character varying`,
);
await queryRunner.query(
`ALTER TABLE "submission" ADD "completedAt" TIMESTAMP WITH TIME ZONE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "submission" DROP COLUMN "completedAt"`,
);
await queryRunner.query(`ALTER TABLE "submission" DROP COLUMN "code"`);
await queryRunner.query(
`ALTER TABLE "submission_answer" DROP COLUMN "createdAt"`,
);
await queryRunner.query(
`ALTER TABLE "choice" ADD "value" integer NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "unq_question_multi_choice" ON "question_transition" ("type", "fromQuestionId") WHERE (type = 'multi-choice'::question_transition_type_enum)`,
);
}
}
@@ -5,7 +5,6 @@ Content-Type: application/json
{ {
"question": 13, "question": 13,
"text": "Option 4", "text": "Option 4",
"value": 1,
"order": 4, "order": 4,
"score": 10 "score": 10
} }
@@ -27,14 +27,6 @@ export class CreateChoiceDTO {
}) })
text!: string; text!: string;
@IsNumber()
@IsNotEmpty({
message: t('validation.requiredField', {
field: '$t(questions.fields.value)',
}),
})
value!: number;
@IsNumber() @IsNumber()
@IsNotEmpty({ @IsNotEmpty({
message: t('validation.requiredField', { message: t('validation.requiredField', {
@@ -16,10 +16,6 @@ export class FindChoicesDTO extends PartialType(PaginationQueryDTO) {
@IsOptional() @IsOptional()
text?: string; text?: string;
@IsNumber()
@IsOptional()
value?: number;
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
choiceOrder?: number; choiceOrder?: number;
@@ -26,9 +26,6 @@ export class Choice {
}) })
text!: string; text!: string;
@Column({ type: 'int', nullable: false })
value!: number;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
order!: number; order!: number;
@@ -3,6 +3,5 @@ export enum ChoiceSortField {
QUESTION = 'question', QUESTION = 'question',
TEXT = 'text', TEXT = 'text',
ORDER = 'order', ORDER = 'order',
VALUE = 'value',
SCORE = 'score', SCORE = 'score',
} }
@@ -17,10 +17,6 @@ export const filterChoices = (query: FindChoicesDTO) => {
where.order = query.choiceOrder; where.order = query.choiceOrder;
} }
if (query.value !== undefined) {
where.value = query.value;
}
if (query.score !== undefined) { if (query.score !== undefined) {
where.score = query.score; where.score = query.score;
} }
@@ -1,5 +1,6 @@
import { import {
Column, Column,
CreateDateColumn,
Entity, Entity,
Index, Index,
JoinTable, JoinTable,
@@ -44,4 +45,7 @@ export class SubmissionAnswer {
nullable: true, nullable: true,
}) })
textValue?: string; textValue?: string;
@CreateDateColumn()
createdAt!: Date;
} }
@@ -19,6 +19,9 @@ export class Submission {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@Column({ type: 'varchar', nullable: true })
code!: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' }) @ManyToOne(() => User, { onDelete: 'CASCADE' })
user!: User; user!: User;
@@ -43,4 +46,7 @@ export class Submission {
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
@Column({ type: 'timestamptz', nullable: true })
completedAt?: Date;
} }
@@ -183,7 +183,11 @@ export class AnswerAuthenticatedProvider {
await manager.update( await manager.update(
Submission, Submission,
{ id: submission.id }, { id: submission.id },
{ currentQuestion: null, status: SubmissionStatus.COMPLETED }, {
currentQuestion: null,
status: SubmissionStatus.COMPLETED,
completedAt: new Date(),
},
); );
const result = const result =
@@ -2,7 +2,6 @@ import { AccessType } from '@/modules/tests/enums/access-types.enum';
import { TestsService } from '@/modules/tests/providers/tests.service'; import { TestsService } from '@/modules/tests/providers/tests.service';
import { User } from '@/modules/users/entities/user.entity'; import { User } from '@/modules/users/entities/user.entity';
import { import {
BadRequestException,
ForbiddenException, ForbiddenException,
Injectable, Injectable,
NotFoundException, NotFoundException,
@@ -12,7 +11,6 @@ import { Submission } from '../../entities/submission.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
import { AppResponse } from '@/common/responses'; import { AppResponse } from '@/common/responses';
import { SubmissionStatus } from '../../enums/submission-status.enum';
import { Test } from '@/modules/tests/entities/test.entity'; import { Test } from '@/modules/tests/entities/test.entity';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { RedisService } from '@/common/modules/redis/providers/redis.service'; import { RedisService } from '@/common/modules/redis/providers/redis.service';
@@ -20,6 +18,7 @@ import { GuestSubmissionSession } from '../../interfaces/guest-submission-sessio
import { TestStatus } from '@/modules/tests/enums/test-status.enum'; import { TestStatus } from '@/modules/tests/enums/test-status.enum';
import { QuestionTransition } from '@/modules/questions/entities/question-transition.entity'; import { QuestionTransition } from '@/modules/questions/entities/question-transition.entity';
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum'; import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
import Utils from '@/common/utils';
@Injectable() @Injectable()
export class StartSubmissionProvider { export class StartSubmissionProvider {
@@ -113,34 +112,13 @@ export class StartSubmissionProvider {
} }
private async startAuthenticated(test: Test, user: User) { private async startAuthenticated(test: Test, user: User) {
let submission: Submission | null = null;
try {
submission = await this.submissionsRepository.findOne({
where: {
test: {
id: test.id,
},
user: {
id: user.id,
},
},
});
} catch (err) {
throw new AppTimeoutException(err);
}
if (submission && submission.status === SubmissionStatus.COMPLETED) {
throw new BadRequestException('submissions.errors.alreadyTookTest');
}
const firstQuestion = await this.getFirstQuestion(test.id); const firstQuestion = await this.getFirstQuestion(test.id);
if (!submission) { const submission = this.submissionsRepository.create({
submission = this.submissionsRepository.create({
test, test,
user: user || undefined, user: user || undefined,
currentQuestion: firstQuestion, currentQuestion: firstQuestion,
code: Utils.StringUtils.generateCode({ prefix: 's-' }),
}); });
try { try {
@@ -148,7 +126,6 @@ export class StartSubmissionProvider {
} catch (err) { } catch (err) {
throw new AppTimeoutException(err); throw new AppTimeoutException(err);
} }
}
return new AppResponse({ return new AppResponse({
submissionId: submission.id, submissionId: submission.id,