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

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 = (
select(Solution)
.filter_by(task_id=task_id)
.options(
selectinload(Solution.files),
)
)
result = await self.db.execute(query)
return result.scalars().all()
@ -44,6 +47,11 @@ class SolutionsRepository:
await self.db.refresh(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:
await self.db.delete(solution)
await self.db.commit()

View File

@ -6,9 +6,9 @@ from starlette.responses import FileResponse
from app.database.session import get_db
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.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.solutions_service import SolutionsService
@ -123,3 +123,20 @@ async def upload_file(
):
task_files_service = SolutionFilesService(db)
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:
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.tasks_repository import TasksRepository
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.settings import Settings
@ -67,6 +67,20 @@ class SolutionsService:
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:
task_model = await self.tasks_repository.get_by_id(task_id)

View File

@ -59,6 +59,14 @@ export const solutionsApi = createApi({
},
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,
useGetSolutionFilesListQuery,
useUploadFileMutation,
useCreateAssessmentMutation,
} = solutionsApi;

View File

@ -4,7 +4,7 @@ import {
Col, Collapse,
Divider,
Empty, Flex,
Form,
Form, Input, InputNumber,
Modal,
Popconfirm,
Row,
@ -47,7 +47,10 @@ const ViewTaskModal = () => {
handleRemoveFile,
handleOk,
draftFiles,
handleDeleSolution
handleDeleSolution,
allSolutions,
onAssessmentFinish,
assessmentForm,
} = useViewTaskModal();
return (
@ -214,9 +217,9 @@ const ViewTaskModal = () => {
onClick={() => downloadFile(file.id, file.filename)}
loading={downloadingFiles[file.id]}
>
<span style={{marginLeft: 8}}>
{file.filename} ({(file.file_size / 1024 / 1024).toFixed(2)} МБ)
</span>
<span style={{marginLeft: 8}}>
{file.filename}
</span>
<DownloadOutlined style={{marginLeft: 8, color: "#1890ff"}}/>
</Button>
))}
@ -259,9 +262,116 @@ const ViewTaskModal = () => {
</Button>
</Col>
) : [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"}}>
<Button onClick={handleClose}>
Закрыть

View File

@ -1,12 +1,13 @@
import {useDispatch, useSelector} from "react-redux";
import {setSelectedTaskToView} from "../../../../../Redux/Slices/tasksSlice.js";
import {notification} from "antd";
import {Form, notification} from "antd";
import CONFIG from "../../../../../Core/сonfig.js";
import {useMemo, useRef, useState} from "react";
import {useGetTaskFilesListQuery} from "../../../../../Api/tasksApi.js";
import {useGetAuthenticatedUserDataQuery} from "../../../../../Api/usersApi.js";
import {
useCreateSolutionMutation, useDeleteSolutionMutation,
useCreateAssessmentMutation,
useCreateSolutionMutation, useDeleteSolutionMutation, useGetTaskSolutionsQuery,
useGetTaskStudentSolutionsQuery,
useUploadFileMutation
} from "../../../../../Api/solutionsApi.js";
@ -20,8 +21,10 @@ const useViewTaskModal = () => {
selectedTaskToView
} = useSelector((state) => state.tasks);
const [assessmentForm] = Form.useForm();
const [
craeteColution,
createSolution,
{
isLoading: isCreatingSolution,
isError: isErrorCreatingSoltion
@ -80,7 +83,7 @@ const useViewTaskModal = () => {
answer_text: content,
};
const response = await craeteColution({
const response = await createSolution({
taskId: selectedTaskToView?.id,
solution: solutionData,
}).unwrap();
@ -154,6 +157,14 @@ const useViewTaskModal = () => {
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 downloadFile = async (fileId, fileName) => {
@ -168,7 +179,8 @@ const useViewTaskModal = () => {
placement: "topRight",
});
return;
};
}
const response = await fetch(`${CONFIG.BASE_URL}/solutions/file/${fileId}/`, {
method: 'GET',
@ -185,7 +197,8 @@ const useViewTaskModal = () => {
placement: "topRight",
});
return;
};
}
const contentType = response.headers.get('content-type');
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(
() => ({
readonly: false,
@ -313,7 +352,10 @@ const useViewTaskModal = () => {
handleRemoveFile,
handleOk,
draftFiles,
handleDeleSolution
handleDeleSolution,
allSolutions,
onAssessmentFinish,
assessmentForm,
}
};