diff --git a/backend/app/api/tests.py b/backend/app/api/tests.py index 4361bb6..8c6bcf6 100644 --- a/backend/app/api/tests.py +++ b/backend/app/api/tests.py @@ -80,6 +80,44 @@ async def create_test(data: TestCreate, db: AsyncSession = Depends(get_db)): return result.scalar_one() +@router.get("/{test_id}/versions", response_model=list[TestListItem]) +async def get_test_versions(test_id: int, db: AsyncSession = Depends(get_db)): + """Возвращает все версии теста (от первой к последней).""" + # Загружаем текущий тест + result = await db.execute(select(Test).where(Test.id == test_id)) + test = result.scalar_one_or_none() + if not test: + raise HTTPException(status_code=404, detail="Тест не найден") + + # Идём вверх до корневой версии + root = test + while root.parent_id is not None: + result = await db.execute(select(Test).where(Test.id == root.parent_id)) + root = result.scalar_one() + + # Идём вниз от корня, собирая цепочку + versions: list[Test] = [] + current = root + while current is not None: + result = await db.execute( + select(Test) + .options(selectinload(Test.questions)) + .where(Test.id == current.id) + ) + current_with_qs = result.scalar_one() + versions.append(current_with_qs) + + result = await db.execute(select(Test).where(Test.parent_id == current.id)) + current = result.scalar_one_or_none() + + items = [] + for v in versions: + item = TestListItem.model_validate(v) + item.questions_count = len(v.questions) + items.append(item) + return items + + @router.put("/{test_id}", response_model=TestUpdateResponse) async def update_test(test_id: int, data: TestCreate, db: AsyncSession = Depends(get_db)): result = await db.execute( diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index e4189e8..19f7e09 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -69,4 +69,5 @@ export const testsApi = { create: (data: CreateTestDto) => client.post('/tests', data), update: (id: number, data: CreateTestDto) => client.put(`/tests/${id}`, data), + versions: (id: number) => client.get(`/tests/${id}/versions`), } diff --git a/frontend/src/pages/TestEdit/index.tsx b/frontend/src/pages/TestEdit/index.tsx index 576494b..20b3b70 100644 --- a/frontend/src/pages/TestEdit/index.tsx +++ b/frontend/src/pages/TestEdit/index.tsx @@ -6,11 +6,12 @@ import { PlayCircleOutlined, } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Alert, Button, Card, Descriptions, List, Space, Spin, Tag, Typography, message } from 'antd' +import { Alert, Button, Card, Descriptions, List, Space, Spin, Table, Tag, Typography, message } from 'antd' +import type { ColumnsType } from 'antd/es/table' import { useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { Answer, CreateTestDto, testsApi } from '../../api/tests' +import { Answer, CreateTestDto, TestListItem, testsApi } from '../../api/tests' import TestForm, { TestFormValues } from '../../components/TestForm' const { Title, Text } = Typography @@ -26,6 +27,12 @@ export default function TestEdit() { queryFn: () => testsApi.get(Number(id)).then((r) => r.data), }) + const { data: versions = [] } = useQuery({ + queryKey: ['tests', id, 'versions'], + queryFn: () => testsApi.versions(Number(id)).then((r) => r.data), + enabled: !editMode, + }) + const { mutate: updateTest, isPending } = useMutation({ mutationFn: (data: CreateTestDto) => testsApi.update(Number(id), data).then((r) => r.data), onSuccess: (result) => { @@ -174,6 +181,66 @@ export default function TestEdit() { /> ))} + + {versions.length > 1 && ( + <> + + История версий + + + dataSource={versions} + rowKey="id" + size="small" + pagination={false} + rowClassName={(record) => (record.id === test.id ? 'ant-table-row-selected' : '')} + columns={ + [ + { + title: 'Версия', + dataIndex: 'version', + width: 90, + render: (v: number, record: TestListItem) => ( + + v{v} + {record.id === test.id && (текущая)} + + ), + }, + { + title: 'Дата', + dataIndex: 'created_at', + width: 120, + render: (d: string) => new Date(d).toLocaleDateString('ru-RU'), + }, + { + title: 'Вопросов', + dataIndex: 'questions_count', + width: 100, + align: 'center' as const, + }, + { + title: 'Порог зачёта', + dataIndex: 'passing_score', + width: 120, + align: 'center' as const, + render: (s: number) => `${s}%`, + }, + { + title: '', + key: 'action', + width: 100, + render: (_: unknown, record: TestListItem) => + record.id !== test.id ? ( + + ) : null, + }, + ] as ColumnsType + } + /> + + )} ) }