From f0bf186b6c2297989fab49c0ff11e9f72bfa2453 Mon Sep 17 00:00:00 2001 From: radmehr Date: Tue, 8 Sep 2026 17:44:47 +0330 Subject: [PATCH] feat: add user submission history preparations --- src/common/utils/string-utils.ts | 21 +++++++++ src/database/seeds/seed.ts | 24 ---------- src/i18n/en/questions.json | 1 - src/i18n/fa/questions.json | 1 - ...1788876455659-PrepUserSubmissionHistory.ts | 35 +++++++++++++++ .../http/admin-choices.post.endpoints.http | 1 - .../questions/dtos/create-choice.dto.ts | 8 ---- .../questions/dtos/find-choices.dto.ts | 4 -- .../questions/entities/choice.entity.ts | 3 -- .../questions/enums/choice-sort-field.enum.ts | 1 - .../helpers/filter-choices.helper.ts | 4 -- .../entities/submission-answer.entity.ts | 4 ++ .../submissions/entities/submission.entity.ts | 6 +++ .../answer/answer-authenticated.provider.ts | 6 ++- .../start/start-submission.provider.ts | 45 +++++-------------- 15 files changed, 82 insertions(+), 82 deletions(-) create mode 100644 src/migrations/1788876455659-PrepUserSubmissionHistory.ts diff --git a/src/common/utils/string-utils.ts b/src/common/utils/string-utils.ts index 73f7706..27d5d3c 100644 --- a/src/common/utils/string-utils.ts +++ b/src/common/utils/string-utils.ts @@ -7,3 +7,24 @@ export const normalizePhone = (phone: string): string => { 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}`; +}; diff --git a/src/database/seeds/seed.ts b/src/database/seeds/seed.ts index e83f701..42e6fed 100644 --- a/src/database/seeds/seed.ts +++ b/src/database/seeds/seed.ts @@ -261,7 +261,6 @@ async function seedTest() { const q1Yes = choiceRepository.create({ question: q1, text: 'بله', - value: 1, order: 1, score: 1, }); @@ -269,7 +268,6 @@ async function seedTest() { const q1No = choiceRepository.create({ question: q1, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -285,7 +283,6 @@ async function seedTest() { const q2Yes = choiceRepository.create({ question: q2, text: 'بله', - value: 1, order: 1, score: 1, }); @@ -293,7 +290,6 @@ async function seedTest() { const q2No = choiceRepository.create({ question: q2, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -301,7 +297,6 @@ async function seedTest() { const q2NotSure = choiceRepository.create({ question: q2, text: 'مطمئن نیستم', - value: 2, order: 3, score: 1, }); @@ -332,7 +327,6 @@ async function seedTest() { choiceRepository.create({ question: q3, text, - value: index + 1, order: index + 1, score: 2, }), @@ -363,7 +357,6 @@ async function seedTest() { choiceRepository.create({ question: q4, text, - value: index + 1, order: index + 1, score: 1, }), @@ -380,7 +373,6 @@ async function seedTest() { const q5Yes = choiceRepository.create({ question: q5, text: 'بله', - value: 1, order: 1, score: 1, }); @@ -388,7 +380,6 @@ async function seedTest() { const q5No = choiceRepository.create({ question: q5, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -396,7 +387,6 @@ async function seedTest() { const q5NotSure = choiceRepository.create({ question: q5, text: 'مطمئن نیستم', - value: 2, order: 3, score: 1, }); @@ -412,7 +402,6 @@ async function seedTest() { const q6Yes = choiceRepository.create({ question: q6, text: 'بله', - value: 1, order: 1, score: 1, }); @@ -420,7 +409,6 @@ async function seedTest() { const q6No = choiceRepository.create({ question: q6, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -428,7 +416,6 @@ async function seedTest() { const q6NotSure = choiceRepository.create({ question: q6, text: 'مطمئن نیستم', - value: 2, order: 3, score: 1, }); @@ -444,7 +431,6 @@ async function seedTest() { const q7Yes = choiceRepository.create({ question: q7, text: 'بله', - value: 1, order: 1, score: 2, }); @@ -452,7 +438,6 @@ async function seedTest() { const q7No = choiceRepository.create({ question: q7, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -468,7 +453,6 @@ async function seedTest() { const q8Yes = choiceRepository.create({ question: q8, text: 'بله', - value: 1, order: 1, score: 2, }); @@ -476,7 +460,6 @@ async function seedTest() { const q8No = choiceRepository.create({ question: q8, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -492,7 +475,6 @@ async function seedTest() { const q10Yes = choiceRepository.create({ question: q10, text: 'بله', - value: 1, order: 1, score: 1, }); @@ -500,7 +482,6 @@ async function seedTest() { const q10No = choiceRepository.create({ question: q10, text: 'خیر', - value: 0, order: 2, score: 0, }); @@ -516,7 +497,6 @@ async function seedTest() { const q12Always = choiceRepository.create({ question: q12, text: 'همیشه با همهٔ شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.', - value: 1, order: 1, score: 0, }); @@ -524,7 +504,6 @@ async function seedTest() { const q12Sometimes = choiceRepository.create({ question: q12, text: 'گاهی با شرکای جنسی‌ام از کاندوم استفاده کرده‌ام.', - value: 2, order: 2, score: 2, }); @@ -532,7 +511,6 @@ async function seedTest() { const q12Never = choiceRepository.create({ question: q12, text: 'هیچ‌وقت با هیچ‌کدام از شرکای جنسی‌ام از کاندوم استفاده نکرده‌ام.', - value: 3, order: 3, score: 5, }); @@ -540,7 +518,6 @@ async function seedTest() { const q12NotApplicable = choiceRepository.create({ question: q12, text: 'سؤال در مورد من صحت ندارد؛ چون که رابطهٔ واژنی یا مقعدی نداشته‌ام.', - value: 4, order: 4, score: 1, }); @@ -548,7 +525,6 @@ async function seedTest() { const q12NotSure = choiceRepository.create({ question: q12, text: 'مطمئن نیستم.', - value: 5, order: 5, score: 2, }); diff --git a/src/i18n/en/questions.json b/src/i18n/en/questions.json index ca81be5..2804d84 100644 --- a/src/i18n/en/questions.json +++ b/src/i18n/en/questions.json @@ -9,7 +9,6 @@ "question": "Question", "text": "Text", "value": "Value", - "choice": "Choice", "score": "Score", "weight": "Weight", "rangeRule": "Range Rule", diff --git a/src/i18n/fa/questions.json b/src/i18n/fa/questions.json index e404539..d5bca68 100644 --- a/src/i18n/fa/questions.json +++ b/src/i18n/fa/questions.json @@ -8,7 +8,6 @@ "metadata": "تنظیمات", "question": "سوال", "text": "متن", - "value": "مقدار", "choice": "گزینه", "score": "نمره", "weight": "وزن", diff --git a/src/migrations/1788876455659-PrepUserSubmissionHistory.ts b/src/migrations/1788876455659-PrepUserSubmissionHistory.ts new file mode 100644 index 0000000..828e78c --- /dev/null +++ b/src/migrations/1788876455659-PrepUserSubmissionHistory.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PrepUserSubmissionHistory1788876455659 implements MigrationInterface { + name = 'PrepUserSubmissionHistory1788876455659'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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)`, + ); + } +} diff --git a/src/modules/admin/choices/http/admin-choices.post.endpoints.http b/src/modules/admin/choices/http/admin-choices.post.endpoints.http index 78089c9..e390eea 100644 --- a/src/modules/admin/choices/http/admin-choices.post.endpoints.http +++ b/src/modules/admin/choices/http/admin-choices.post.endpoints.http @@ -5,7 +5,6 @@ Content-Type: application/json { "question": 13, "text": "Option 4", - "value": 1, "order": 4, "score": 10 } \ No newline at end of file diff --git a/src/modules/questions/dtos/create-choice.dto.ts b/src/modules/questions/dtos/create-choice.dto.ts index 478abfd..5e54397 100644 --- a/src/modules/questions/dtos/create-choice.dto.ts +++ b/src/modules/questions/dtos/create-choice.dto.ts @@ -27,14 +27,6 @@ export class CreateChoiceDTO { }) text!: string; - @IsNumber() - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(questions.fields.value)', - }), - }) - value!: number; - @IsNumber() @IsNotEmpty({ message: t('validation.requiredField', { diff --git a/src/modules/questions/dtos/find-choices.dto.ts b/src/modules/questions/dtos/find-choices.dto.ts index dd9bae8..e6e3c2b 100644 --- a/src/modules/questions/dtos/find-choices.dto.ts +++ b/src/modules/questions/dtos/find-choices.dto.ts @@ -16,10 +16,6 @@ export class FindChoicesDTO extends PartialType(PaginationQueryDTO) { @IsOptional() text?: string; - @IsNumber() - @IsOptional() - value?: number; - @IsNumber() @IsOptional() choiceOrder?: number; diff --git a/src/modules/questions/entities/choice.entity.ts b/src/modules/questions/entities/choice.entity.ts index fbaf153..53039a7 100644 --- a/src/modules/questions/entities/choice.entity.ts +++ b/src/modules/questions/entities/choice.entity.ts @@ -26,9 +26,6 @@ export class Choice { }) text!: string; - @Column({ type: 'int', nullable: false }) - value!: number; - @Column({ type: 'int', nullable: false }) order!: number; diff --git a/src/modules/questions/enums/choice-sort-field.enum.ts b/src/modules/questions/enums/choice-sort-field.enum.ts index b6c4b46..89a0f8c 100644 --- a/src/modules/questions/enums/choice-sort-field.enum.ts +++ b/src/modules/questions/enums/choice-sort-field.enum.ts @@ -3,6 +3,5 @@ export enum ChoiceSortField { QUESTION = 'question', TEXT = 'text', ORDER = 'order', - VALUE = 'value', SCORE = 'score', } diff --git a/src/modules/questions/helpers/filter-choices.helper.ts b/src/modules/questions/helpers/filter-choices.helper.ts index e6d2e78..560038d 100644 --- a/src/modules/questions/helpers/filter-choices.helper.ts +++ b/src/modules/questions/helpers/filter-choices.helper.ts @@ -17,10 +17,6 @@ export const filterChoices = (query: FindChoicesDTO) => { where.order = query.choiceOrder; } - if (query.value !== undefined) { - where.value = query.value; - } - if (query.score !== undefined) { where.score = query.score; } diff --git a/src/modules/submissions/entities/submission-answer.entity.ts b/src/modules/submissions/entities/submission-answer.entity.ts index d9a8505..949fdf1 100644 --- a/src/modules/submissions/entities/submission-answer.entity.ts +++ b/src/modules/submissions/entities/submission-answer.entity.ts @@ -1,5 +1,6 @@ import { Column, + CreateDateColumn, Entity, Index, JoinTable, @@ -44,4 +45,7 @@ export class SubmissionAnswer { nullable: true, }) textValue?: string; + + @CreateDateColumn() + createdAt!: Date; } diff --git a/src/modules/submissions/entities/submission.entity.ts b/src/modules/submissions/entities/submission.entity.ts index 04258f1..2d0d143 100644 --- a/src/modules/submissions/entities/submission.entity.ts +++ b/src/modules/submissions/entities/submission.entity.ts @@ -19,6 +19,9 @@ export class Submission { @PrimaryGeneratedColumn() id!: number; + @Column({ type: 'varchar', nullable: true }) + code!: string; + @ManyToOne(() => User, { onDelete: 'CASCADE' }) user!: User; @@ -43,4 +46,7 @@ export class Submission { @CreateDateColumn() createdAt!: Date; + + @Column({ type: 'timestamptz', nullable: true }) + completedAt?: Date; } diff --git a/src/modules/submissions/providers/answer/answer-authenticated.provider.ts b/src/modules/submissions/providers/answer/answer-authenticated.provider.ts index 240aa18..97faea4 100644 --- a/src/modules/submissions/providers/answer/answer-authenticated.provider.ts +++ b/src/modules/submissions/providers/answer/answer-authenticated.provider.ts @@ -183,7 +183,11 @@ export class AnswerAuthenticatedProvider { await manager.update( Submission, { id: submission.id }, - { currentQuestion: null, status: SubmissionStatus.COMPLETED }, + { + currentQuestion: null, + status: SubmissionStatus.COMPLETED, + completedAt: new Date(), + }, ); const result = diff --git a/src/modules/submissions/providers/start/start-submission.provider.ts b/src/modules/submissions/providers/start/start-submission.provider.ts index 4e1ff60..4dc0f52 100644 --- a/src/modules/submissions/providers/start/start-submission.provider.ts +++ b/src/modules/submissions/providers/start/start-submission.provider.ts @@ -2,7 +2,6 @@ import { AccessType } from '@/modules/tests/enums/access-types.enum'; import { TestsService } from '@/modules/tests/providers/tests.service'; import { User } from '@/modules/users/entities/user.entity'; import { - BadRequestException, ForbiddenException, Injectable, NotFoundException, @@ -12,7 +11,6 @@ import { Submission } from '../../entities/submission.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { AppResponse } from '@/common/responses'; -import { SubmissionStatus } from '../../enums/submission-status.enum'; import { Test } from '@/modules/tests/entities/test.entity'; import { randomUUID } from 'crypto'; 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 { QuestionTransition } from '@/modules/questions/entities/question-transition.entity'; import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum'; +import Utils from '@/common/utils'; @Injectable() export class StartSubmissionProvider { @@ -113,41 +112,19 @@ export class StartSubmissionProvider { } 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); - if (!submission) { - submission = this.submissionsRepository.create({ - test, - user: user || undefined, - currentQuestion: firstQuestion, - }); + const submission = this.submissionsRepository.create({ + test, + user: user || undefined, + currentQuestion: firstQuestion, + code: Utils.StringUtils.generateCode({ prefix: 's-' }), + }); - try { - await this.submissionsRepository.save(submission); - } catch (err) { - throw new AppTimeoutException(err); - } + try { + await this.submissionsRepository.save(submission); + } catch (err) { + throw new AppTimeoutException(err); } return new AppResponse({