feat: полный бэк и фронт (попытки, разбор, импорт, ИИ, назначения)
- Сервисы: testAttemptService, testAccess, document import/gen/extract, LLM, assignment, aiEditor - Конфиг: devAuthor, featureFlags; messages/ru; интеграция V.9 (skip без БД) - API/роуты: app, auth, server; Dockerfile и env example - Фронт: TestAttempt, TestAttemptReview, AttemptReviewBlock, стили, правки App/api/login/vite - compose и README; смоук-тесты расширены Закрывает отсутствие модулей в origin после клона. Made-with: Cursor
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
* V.3 saveTestDraft, fork версии, контент вопросов.
|
||||
*/
|
||||
import { hasAnyAttemptForTest } from './testChainService.js';
|
||||
import { RU } from '../messages/ru.js';
|
||||
import { isTestAuthor } from '../config/devAuthor.js';
|
||||
|
||||
/**
|
||||
* @param {import('pg').PoolClient} client
|
||||
@@ -93,23 +95,25 @@ export async function replaceVersionContent(client, testVersionId, payload) {
|
||||
export async function forkNewVersion(client, testId) {
|
||||
const av = await getActiveVersionRow(client, testId);
|
||||
if (!av) {
|
||||
throw new Error('no active version');
|
||||
throw new Error(RU.noActiveVersion);
|
||||
}
|
||||
const { rows: mx } = await client.query(
|
||||
`SELECT COALESCE(MAX(version), 0) AS v FROM test_versions WHERE test_id = $1`,
|
||||
[testId]
|
||||
);
|
||||
const nextV = (mx[0].v || 0) + 1;
|
||||
// Сначала снять is_active с цепочки: частичный уникальный индекс
|
||||
// uq_test_versions_one_active_per_test — не более одной true на test_id.
|
||||
await client.query(
|
||||
`UPDATE test_versions SET is_active = false WHERE test_id = $1`,
|
||||
[testId]
|
||||
);
|
||||
const { rows: nv } = await client.query(
|
||||
`INSERT INTO test_versions (test_id, version, is_active, parent_id)
|
||||
VALUES ($1, $2, true, $3) RETURNING *`,
|
||||
[testId, nextV, av.id]
|
||||
);
|
||||
const newRow = nv[0];
|
||||
await client.query(
|
||||
`UPDATE test_versions SET is_active = false WHERE test_id = $1 AND id <> $2`,
|
||||
[testId, newRow.id]
|
||||
);
|
||||
await copyQuestionTree(client, av.id, newRow.id);
|
||||
return newRow;
|
||||
}
|
||||
@@ -126,13 +130,13 @@ export async function saveTestDraft(pool, authorId, testId, payload) {
|
||||
[testId]
|
||||
);
|
||||
if (!tr.length) {
|
||||
const e = new Error('Test not found');
|
||||
const e = new Error(RU.testNotFound);
|
||||
e.status = 404;
|
||||
throw e;
|
||||
}
|
||||
const t = tr[0];
|
||||
if (t.created_by !== authorId) {
|
||||
const e = new Error('Forbidden');
|
||||
if (!isTestAuthor(t.created_by, authorId)) {
|
||||
const e = new Error(RU.forbidden);
|
||||
e.status = 403;
|
||||
throw e;
|
||||
}
|
||||
@@ -148,10 +152,20 @@ export async function saveTestDraft(pool, authorId, testId, payload) {
|
||||
[testId, payload.title ?? null, payload.description ?? null]
|
||||
);
|
||||
}
|
||||
if (payload.passingThreshold !== undefined && payload.passingThreshold !== null) {
|
||||
const raw = Number(payload.passingThreshold);
|
||||
if (Number.isFinite(raw)) {
|
||||
const pt = Math.max(0, Math.min(100, Math.round(raw)));
|
||||
await client.query(
|
||||
`UPDATE tests SET passing_threshold = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1`,
|
||||
[testId, pt]
|
||||
);
|
||||
}
|
||||
}
|
||||
const hasAttempts = await hasAnyAttemptForTest(client, testId);
|
||||
let versionRow = await getActiveVersionRow(client, testId);
|
||||
if (!versionRow) {
|
||||
const e = new Error('No active version');
|
||||
const e = new Error(RU.noActiveVersion);
|
||||
e.status = 500;
|
||||
throw e;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user