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