сделал выставление оценок

This commit is contained in:
Андрей Дувакин 2025-11-29 17:42:16 +05:00
parent 45a4f278ca
commit 9195f4eacc
7 changed files with 221 additions and 17 deletions

View File

@ -23,6 +23,9 @@ class SolutionsRepository:
query = ( query = (
select(Solution) select(Solution)
.filter_by(task_id=task_id) .filter_by(task_id=task_id)
.options(
selectinload(Solution.files),
)
) )
result = await self.db.execute(query) result = await self.db.execute(query)
return result.scalars().all() return result.scalars().all()
@ -44,6 +47,11 @@ class SolutionsRepository:
await self.db.refresh(solution) await self.db.refresh(solution)
return solution return solution
async def update(self, solution: Solution) -> Solution:
await self.db.merge(solution)
await self.db.commit()
return solution
async def delete(self, solution: Solution) -> Solution: async def delete(self, solution: Solution) -> Solution:
await self.db.delete(solution) await self.db.delete(solution)
await self.db.commit() await self.db.commit()

View File

@ -6,9 +6,9 @@ from starlette.responses import FileResponse
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.solution_files import ReadSolutionFile from app.domain.entities.solution_files import ReadSolutionFile
from app.domain.entities.solutions import SolutionCreate, SolutionRead, SolutionAfterCreate from app.domain.entities.solutions import SolutionCreate, SolutionRead, SolutionAfterCreate, AssessmentCreate
from app.domain.models import User from app.domain.models import User
from app.infrastructure.dependencies import require_auth_user from app.infrastructure.dependencies import require_auth_user, require_teacher
from app.infrastructure.solution_files_service import SolutionFilesService from app.infrastructure.solution_files_service import SolutionFilesService
from app.infrastructure.solutions_service import SolutionsService from app.infrastructure.solutions_service import SolutionsService
@ -123,3 +123,20 @@ async def upload_file(
): ):
task_files_service = SolutionFilesService(db) task_files_service = SolutionFilesService(db)
return await task_files_service.upload_file(task_id, file) return await task_files_service.upload_file(task_id, file)
@solution_router.post(
'/assessment/{solution_id}/',
status_code=status.HTTP_204_NO_CONTENT,
summary='Set assessment for solution',
description='Set assessment for solution',
)
async def create_assessment(
solution_id: int,
assessment_data: AssessmentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_teacher),
):
solutions_service = SolutionsService(db)
await solutions_service.create_assessment(solution_id, assessment_data, current_user)
return Response(status_code=status.HTTP_204_NO_CONTENT)

View File

@ -34,3 +34,7 @@ class SolutionRead(SolutionAfterCreate):
class Config: class Config:
from_attributes = True from_attributes = True
class AssessmentCreate(BaseModel):
assessment: int = Field(...)

View File

@ -8,7 +8,7 @@ from app.application.solution_files_repository import SolutionFilesRepository
from app.application.solutions_repository import SolutionsRepository from app.application.solutions_repository import SolutionsRepository
from app.application.tasks_repository import TasksRepository from app.application.tasks_repository import TasksRepository
from app.application.users_repository import UsersRepository from app.application.users_repository import UsersRepository
from app.domain.entities.solutions import SolutionRead, SolutionCreate, SolutionAfterCreate from app.domain.entities.solutions import SolutionRead, SolutionCreate, SolutionAfterCreate, AssessmentCreate
from app.domain.models import User, Solution from app.domain.models import User, Solution
from app.settings import Settings from app.settings import Settings
@ -67,6 +67,20 @@ class SolutionsService:
return response return response
async def create_assessment(self, solution_id: int, assessment: AssessmentCreate, user: User) -> None:
solution_model = await self.solutions_repository.get_by_id(solution_id)
if solution_model is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Такого решения не найдено"
)
solution_model.assessment = assessment.assessment
solution_model.assessment_autor_id = user.id
await self.solutions_repository.update(solution_model)
async def create(self, solution: SolutionCreate, creator: User, task_id: int) -> SolutionAfterCreate: async def create(self, solution: SolutionCreate, creator: User, task_id: int) -> SolutionAfterCreate:
task_model = await self.tasks_repository.get_by_id(task_id) task_model = await self.tasks_repository.get_by_id(task_id)

View File

@ -59,6 +59,14 @@ export const solutionsApi = createApi({
}, },
invalidatesTags: ["task"], invalidatesTags: ["task"],
}), }),
createAssessment: builder.mutation({
query: ({solutionId, assessment}) => ({
url: `/solutions/assessment/${solutionId}/`,
method: "POST",
body: assessment,
}),
invalidatesTags: ["lesson"],
}),
}), }),
}); });
@ -69,5 +77,6 @@ export const {
useDeleteSolutionMutation, useDeleteSolutionMutation,
useGetSolutionFilesListQuery, useGetSolutionFilesListQuery,
useUploadFileMutation, useUploadFileMutation,
useCreateAssessmentMutation,
} = solutionsApi; } = solutionsApi;

View File

@ -4,7 +4,7 @@ import {
Col, Collapse, Col, Collapse,
Divider, Divider,
Empty, Flex, Empty, Flex,
Form, Form, Input, InputNumber,
Modal, Modal,
Popconfirm, Popconfirm,
Row, Row,
@ -47,7 +47,10 @@ const ViewTaskModal = () => {
handleRemoveFile, handleRemoveFile,
handleOk, handleOk,
draftFiles, draftFiles,
handleDeleSolution handleDeleSolution,
allSolutions,
onAssessmentFinish,
assessmentForm,
} = useViewTaskModal(); } = useViewTaskModal();
return ( return (
@ -215,7 +218,7 @@ const ViewTaskModal = () => {
loading={downloadingFiles[file.id]} loading={downloadingFiles[file.id]}
> >
<span style={{marginLeft: 8}}> <span style={{marginLeft: 8}}>
{file.filename} ({(file.file_size / 1024 / 1024).toFixed(2)} МБ) {file.filename}
</span> </span>
<DownloadOutlined style={{marginLeft: 8, color: "#1890ff"}}/> <DownloadOutlined style={{marginLeft: 8, color: "#1890ff"}}/>
</Button> </Button>
@ -259,9 +262,116 @@ const ViewTaskModal = () => {
</Button> </Button>
</Col> </Col>
) : [ROLES.ADMIN, ROLES.TEACHER].includes(currentUser?.role?.title) && ( ) : [ROLES.ADMIN, ROLES.TEACHER].includes(currentUser?.role?.title) && (
<></> <Col>
<Title level={3}>Присланные решения</Title>
{allSolutions.length > 0 ? (
<Collapse accordion>
{allSolutions.map((solution) => (
<Panel
key={solution.id}
header={
<Flex justify="space-between" align="center">
<Text strong>Решение
от {new Date(solution.created_at).toLocaleString("ru-RU")}</Text>
{solution.assessment !== null ? (
<Tag
color={solution.assessment >= 80 ? "green" : solution.assessment >= 60 ? "orange" : "red"}>
Оценка: {solution.assessment} / 100
</Tag>
) : (
<Tag color="red">Ждет проверки</Tag>
)} )}
</Flex>
}
extra={
solution.assessment !== null && (
<Tag color="purple">
Проверено: {solution.assessment_autor?.first_name} {solution.assessment_autor?.last_name}
</Tag>
)
}
>
<div style={{marginBottom: 16}}>
<Text strong>Ответ:</Text>
<div
style={{
background: "#f9f9f9",
padding: 16,
borderRadius: 8,
margin: "12px 0",
border: "1px solid #f0f0f0",
minHeight: 60,
}}
dangerouslySetInnerHTML={{__html: solution.answer_text || "<em>Текст ответа отсутствует</em>"}}
/>
</div>
{solution.files && solution.files.length > 0 ? (
<div>
<Text strong>Прикреплённые файлы:</Text>
<div style={{
marginTop: 8,
display: "flex",
flexDirection: "column",
gap: 8
}}>
{solution.files.map((file) => (
<Button
key={file.id}
type="dashed"
icon={<FileOutlined/>}
style={{textAlign: "left"}}
onClick={() => downloadFile(file.id, file.filename)}
loading={downloadingFiles[file.id]}
>
<span style={{marginLeft: 8}}>
{file.filename}
</span>
<DownloadOutlined style={{marginLeft: 8, color: "#1890ff"}}/>
</Button>
))}
</div>
</div>
) : (
<Text type="secondary">Файлы не прикреплены</Text>
)}
<Title level={3}>Оценка</Title>
<Form form={assessmentForm} name={"assessmentForm"} onFinish={() => {
onAssessmentFinish(solution.id)
}}>
<Form.Item
name={"assessment"}
rules={[{required: true, message: "Укажите оценку"}]}
>
<InputNumber
min={1}
max={100}
placeholder={"Выставите балл от 1 до 100"}
style={{
minWidth: "230px"
}}
defaultValue={solution.assessment || null}
required
/>
</Form.Item>
<Form.Item>
<Button type={"primary"} htmlType={"submit"}>
Выставить оценку
</Button>
</Form.Item>
</Form>
</Panel>
))}
</Collapse>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="Решений пока нет"
/>
)}
</Col>
)}
<Divider/>
<div style={{textAlign: "right"}}> <div style={{textAlign: "right"}}>
<Button onClick={handleClose}> <Button onClick={handleClose}>
Закрыть Закрыть

View File

@ -1,12 +1,13 @@
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {setSelectedTaskToView} from "../../../../../Redux/Slices/tasksSlice.js"; import {setSelectedTaskToView} from "../../../../../Redux/Slices/tasksSlice.js";
import {notification} from "antd"; import {Form, notification} from "antd";
import CONFIG from "../../../../../Core/сonfig.js"; import CONFIG from "../../../../../Core/сonfig.js";
import {useMemo, useRef, useState} from "react"; import {useMemo, useRef, useState} from "react";
import {useGetTaskFilesListQuery} from "../../../../../Api/tasksApi.js"; import {useGetTaskFilesListQuery} from "../../../../../Api/tasksApi.js";
import {useGetAuthenticatedUserDataQuery} from "../../../../../Api/usersApi.js"; import {useGetAuthenticatedUserDataQuery} from "../../../../../Api/usersApi.js";
import { import {
useCreateSolutionMutation, useDeleteSolutionMutation, useCreateAssessmentMutation,
useCreateSolutionMutation, useDeleteSolutionMutation, useGetTaskSolutionsQuery,
useGetTaskStudentSolutionsQuery, useGetTaskStudentSolutionsQuery,
useUploadFileMutation useUploadFileMutation
} from "../../../../../Api/solutionsApi.js"; } from "../../../../../Api/solutionsApi.js";
@ -20,8 +21,10 @@ const useViewTaskModal = () => {
selectedTaskToView selectedTaskToView
} = useSelector((state) => state.tasks); } = useSelector((state) => state.tasks);
const [assessmentForm] = Form.useForm();
const [ const [
craeteColution, createSolution,
{ {
isLoading: isCreatingSolution, isLoading: isCreatingSolution,
isError: isErrorCreatingSoltion isError: isErrorCreatingSoltion
@ -80,7 +83,7 @@ const useViewTaskModal = () => {
answer_text: content, answer_text: content,
}; };
const response = await craeteColution({ const response = await createSolution({
taskId: selectedTaskToView?.id, taskId: selectedTaskToView?.id,
solution: solutionData, solution: solutionData,
}).unwrap(); }).unwrap();
@ -154,6 +157,14 @@ const useViewTaskModal = () => {
pollingInterval: 5000, pollingInterval: 5000,
}); });
const {
data: allSolutions = [],
isLoading: isAllSolutionsLoading,
} = useGetTaskSolutionsQuery(selectedTaskToView?.id, {
skip: !selectedTaskToView?.id || ![ROLES.TEACHER, ROLES.ADMIN].includes(currentUser?.role?.title),
pollingInterval: 5000,
})
const [downloadingFiles, setDownloadingFiles] = useState({}); const [downloadingFiles, setDownloadingFiles] = useState({});
const downloadFile = async (fileId, fileName) => { const downloadFile = async (fileId, fileName) => {
@ -168,7 +179,8 @@ const useViewTaskModal = () => {
placement: "topRight", placement: "topRight",
}); });
return; return;
}; }
const response = await fetch(`${CONFIG.BASE_URL}/solutions/file/${fileId}/`, { const response = await fetch(`${CONFIG.BASE_URL}/solutions/file/${fileId}/`, {
method: 'GET', method: 'GET',
@ -185,7 +197,8 @@ const useViewTaskModal = () => {
placement: "topRight", placement: "topRight",
}); });
return; return;
}; }
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if (!contentType || contentType.includes('text/html')) { if (!contentType || contentType.includes('text/html')) {
@ -231,6 +244,32 @@ const useViewTaskModal = () => {
} }
}; };
const [
createAssessment,
] = useCreateAssessmentMutation();
const onAssessmentFinish = async (solutionId) => {
const values = await assessmentForm.validateFields();
try {
await createAssessment({
solutionId,
assessment: values,
});
notification.success({
title: "Успех",
description: "Оценка выставлена",
placement: "topRight",
});
} catch (e) {
notification.error({
title: "Ошибка",
description: e.message || "Не удалось выставить оценку",
placement: "topRight",
});
}
};
const joditConfig = useMemo( const joditConfig = useMemo(
() => ({ () => ({
readonly: false, readonly: false,
@ -313,7 +352,10 @@ const useViewTaskModal = () => {
handleRemoveFile, handleRemoveFile,
handleOk, handleOk,
draftFiles, draftFiles,
handleDeleSolution handleDeleSolution,
allSolutions,
onAssessmentFinish,
assessmentForm,
} }
}; };