сделал выставление оценок
This commit is contained in:
parent
45a4f278ca
commit
9195f4eacc
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -34,3 +34,7 @@ class SolutionRead(SolutionAfterCreate):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AssessmentCreate(BaseModel):
|
||||
assessment: int = Field(...)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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}>
|
||||
Закрыть
|
||||
|
||||
@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user