исправил различные функции
This commit is contained in:
parent
7bd89686a9
commit
f4e3cbf3be
4
api/.dockerignore
Normal file
4
api/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.venv
|
||||||
|
k8s
|
||||||
|
.idea
|
||||||
|
.env
|
||||||
18
api/app/Dockerfile
Normal file
18
api/app/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libpq-dev \
|
||||||
|
libmagic1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY req.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r req.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host=0.0.0.0", "--port=8000"]
|
||||||
@ -42,6 +42,7 @@ class SolutionCommentCreate(SolutionCommentBase):
|
|||||||
class SolutionCommentRead(SolutionCommentBase):
|
class SolutionCommentRead(SolutionCommentBase):
|
||||||
comment_autor_id: int
|
comment_autor_id: int
|
||||||
solution_id: int
|
solution_id: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
comment_autor: UserRead
|
comment_autor: UserRead
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ def start_app():
|
|||||||
|
|
||||||
api_app.add_middleware(
|
api_app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=['*'],
|
allow_origins=['https://api.lectio.numerum.team', 'https://lectio.numerum.team', 'http://localhost:5173'],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=['*'],
|
allow_methods=['*'],
|
||||||
allow_headers=['*'],
|
allow_headers=['*'],
|
||||||
|
|||||||
5
api/k8s/helm/lectio-api/Chart.yaml
Normal file
5
api/k8s/helm/lectio-api/Chart.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: lectio-api-app
|
||||||
|
description: Lectio API project
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0"
|
||||||
52
api/k8s/helm/lectio-api/templates/deployment.yaml
Normal file
52
api/k8s/helm/lectio-api/templates/deployment.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ .Chart.Name }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- containerPort: {{ .Values.service.port }}
|
||||||
|
env:
|
||||||
|
- name: SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: lectio-api-secret
|
||||||
|
key: SECRET_KEY
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: lectio-api-secret
|
||||||
|
key: DB_PASSWORD
|
||||||
|
- name: DB_DRIVER
|
||||||
|
value: "{{ .Values.env.DB_DRIVER }}"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "{{ .Values.env.DB_HOST }}"
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "{{ .Values.env.DB_PORT }}"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "{{ .Values.env.DB_USER }}"
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "{{ .Values.env.DB_NAME }}"
|
||||||
|
- name: DB_SCHEMA
|
||||||
|
value: "{{ .Values.env.DB_SCHEMA }}"
|
||||||
|
volumeMounts:
|
||||||
|
- name: uploads-volume
|
||||||
|
mountPath: {{ .Values.persistence.uploads.containerPath }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumes:
|
||||||
|
- name: uploads-volume
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: "{{ .Release.Name }}-uploads-pvc"
|
||||||
23
api/k8s/helm/lectio-api/templates/ingress.yaml
Normal file
23
api/k8s/helm/lectio-api/templates/ingress.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: lectio-api-ingress
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: lets-encrypt
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.ingress.domain }}
|
||||||
|
secretName: {{ .Values.ingress.secretTLSName }}
|
||||||
|
ingressClassName: public
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.domain }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: {{ .Values.ingress.path }}
|
||||||
|
pathType: {{ .Values.ingress.pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ .Chart.Name }}-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.service.port }}
|
||||||
28
api/k8s/helm/lectio-api/templates/pvc.yaml
Normal file
28
api/k8s/helm/lectio-api/templates/pvc.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{{- if .Values.persistence.uploads.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: {{ .Release.Name }}-uploads-pv
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: {{ .Values.persistence.uploads.size }}
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: microk8s-hostpath
|
||||||
|
hostPath:
|
||||||
|
path: {{ .Values.persistence.path }}/uploads
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ .Release.Name }}-uploads-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.uploads.size }}
|
||||||
|
volumeName: {{ .Release.Name }}-uploads-pv
|
||||||
|
{{- end }}
|
||||||
11
api/k8s/helm/lectio-api/templates/service.yaml
Normal file
11
api/k8s/helm/lectio-api/templates/service.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .Chart.Name }}-service
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: {{ .Values.service.port }}
|
||||||
|
selector:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
38
api/k8s/helm/lectio-api/values.yaml
Normal file
38
api/k8s/helm/lectio-api/values.yaml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: andreiduvakin/lectio-api
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8000
|
||||||
|
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128Mi
|
||||||
|
cpu: 200m
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
path: /mnt/k8s_storage/lectio-api
|
||||||
|
uploads:
|
||||||
|
enabled: true
|
||||||
|
size: 7Gi
|
||||||
|
containerPath: /app/uploads
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
secretTLSName: lectio-api-tls-secret
|
||||||
|
|
||||||
|
domain: api.lectio.numerum.team
|
||||||
|
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
|
|
||||||
|
env:
|
||||||
|
DB_DRIVER: postgresql+asyncpg
|
||||||
|
DB_HOST: db.numerum.team
|
||||||
|
DB_PORT: 30000
|
||||||
|
DB_USER: lectio
|
||||||
|
DB_NAME: lectio
|
||||||
|
DB_SCHEMA: public
|
||||||
@ -8,3 +8,5 @@ pyjwt==2.9.0
|
|||||||
fastapi==0.115.0
|
fastapi==0.115.0
|
||||||
pydantic[email]==2.11.4
|
pydantic[email]==2.11.4
|
||||||
aiofiles==25.1.0
|
aiofiles==25.1.0
|
||||||
|
uvicorn==0.34.0
|
||||||
|
python-multipart==0.0.12
|
||||||
7
web/.dockerignore
Normal file
7
web/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
build
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
k8s
|
||||||
|
.env
|
||||||
27
web/Dockerfile
Normal file
27
web/Dockerfile
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG VITE_BASE_URL
|
||||||
|
ENV VITE_BASE_URL=https://api.lectio.numerum.team/api/v1
|
||||||
|
ARG VITE_ROOT_ROLE_NAME
|
||||||
|
ENV VITE_ROOT_ROLE_NAME=root
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
COPY --from=builder /dist /app
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["serve", "-s", ".", "-l", "3000"]
|
||||||
5
web/k8s/helm/lectio-web/Chart.yaml
Normal file
5
web/k8s/helm/lectio-web/Chart.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: lectio-web-app
|
||||||
|
description: Lectio WEB project
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0"
|
||||||
22
web/k8s/helm/lectio-web/templates/deployment.yaml
Normal file
22
web/k8s/helm/lectio-web/templates/deployment.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ .Chart.Name }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- containerPort: {{ .Values.service.port }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
23
web/k8s/helm/lectio-web/templates/ingress.yaml
Normal file
23
web/k8s/helm/lectio-web/templates/ingress.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: visus-api-ingress
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: lets-encrypt
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.ingress.domain }}
|
||||||
|
secretName: {{ .Values.ingress.secretTLSName }}
|
||||||
|
ingressClassName: public
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.domain }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: {{ .Values.ingress.path }}
|
||||||
|
pathType: {{ .Values.ingress.pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ .Chart.Name }}-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.service.port }}
|
||||||
11
web/k8s/helm/lectio-web/templates/service.yaml
Normal file
11
web/k8s/helm/lectio-web/templates/service.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .Chart.Name }}-service
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: {{ .Values.service.port }}
|
||||||
|
selector:
|
||||||
|
app: {{ .Chart.Name }}
|
||||||
23
web/k8s/helm/lectio-web/values.yaml
Normal file
23
web/k8s/helm/lectio-web/values.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: andreiduvakin/lectio-web
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128Mi
|
||||||
|
cpu: 200m
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
secretTLSName: lectio-web-tls-secret
|
||||||
|
|
||||||
|
domain: lectio.numerum.team
|
||||||
|
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
@ -8,6 +8,7 @@ import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
|
|||||||
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
|
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
|
||||||
import CourseDetailPage from "../Components/Pages/CourseDetailPage/CourseDetailPage.jsx";
|
import CourseDetailPage from "../Components/Pages/CourseDetailPage/CourseDetailPage.jsx";
|
||||||
import CoursesPage from "../Components/Pages/CoursesPage/CoursesPage.jsx";
|
import CoursesPage from "../Components/Pages/CoursesPage/CoursesPage.jsx";
|
||||||
|
import GradebookPage from "../Components/Pages/GradebookPage/GradebookPage.jsx";
|
||||||
|
|
||||||
|
|
||||||
const AppRouter = () => (
|
const AppRouter = () => (
|
||||||
@ -20,6 +21,7 @@ const AppRouter = () => (
|
|||||||
<Route path={"/courses"} element={<CoursesPage/>}/>
|
<Route path={"/courses"} element={<CoursesPage/>}/>
|
||||||
<Route path={"/profile"} element={<ProfilePage/>}/>
|
<Route path={"/profile"} element={<ProfilePage/>}/>
|
||||||
<Route path="/courses/:courseId" element={<CourseDetailPage />} />
|
<Route path="/courses/:courseId" element={<CourseDetailPage />} />
|
||||||
|
<Route path="/courses/:courseId/gradebook" element={<GradebookPage />} />
|
||||||
<Route path={"*"} element={<Navigate to={"/courses"}/>}/>
|
<Route path={"*"} element={<Navigate to={"/courses"}/>}/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -52,7 +52,9 @@ const ViewTaskModal = () => {
|
|||||||
onAssessmentFinish,
|
onAssessmentFinish,
|
||||||
assessmentForm,
|
assessmentForm,
|
||||||
onCommentSubmit,
|
onCommentSubmit,
|
||||||
commentForm
|
commentForm,
|
||||||
|
setComment,
|
||||||
|
comment,
|
||||||
} = useViewTaskModal();
|
} = useViewTaskModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -230,7 +232,7 @@ const ViewTaskModal = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Text type="secondary">Файлы не прикреплены</Text>
|
<Text type="secondary">Файлы не прикреплены</Text>
|
||||||
)}
|
)}
|
||||||
<div style={{ marginTop: 32 }}>
|
<div style={{marginTop: 32}}>
|
||||||
<Title level={4}>Комментарии к решению</Title>
|
<Title level={4}>Комментарии к решению</Title>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -247,10 +249,13 @@ const ViewTaskModal = () => {
|
|||||||
<List
|
<List
|
||||||
dataSource={solution.solution_comments}
|
dataSource={solution.solution_comments}
|
||||||
renderItem={(comment) => (
|
renderItem={(comment) => (
|
||||||
<List.Item style={{ padding: "12px 16px", borderBottom: "1px solid #f0f0f0" }}>
|
<List.Item style={{
|
||||||
|
padding: "12px 16px",
|
||||||
|
borderBottom: "1px solid #f0f0f0"
|
||||||
|
}}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar style={{ backgroundColor: "#1890ff" }}>
|
<Avatar style={{backgroundColor: "#1890ff"}}>
|
||||||
{comment.comment_autor.first_name[0]}
|
{comment.comment_autor.first_name[0]}
|
||||||
{comment.comment_autor.last_name[0]}
|
{comment.comment_autor.last_name[0]}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@ -261,12 +266,13 @@ const ViewTaskModal = () => {
|
|||||||
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
||||||
</Text>
|
</Text>
|
||||||
{comment.comment_autor.role?.title === "teacher" && (
|
{comment.comment_autor.role?.title === "teacher" && (
|
||||||
<Tag color="gold" size="small">Преподаватель</Tag>
|
<Tag color="gold"
|
||||||
|
size="small">Преподаватель</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{fontSize: 12}}>
|
||||||
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@ -278,7 +284,7 @@ const ViewTaskModal = () => {
|
|||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
|
dangerouslySetInnerHTML={{__html: comment.comment_text}}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
@ -287,33 +293,23 @@ const ViewTaskModal = () => {
|
|||||||
<Empty
|
<Empty
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
description="Пока нет комментариев"
|
description="Пока нет комментариев"
|
||||||
style={{ margin: "20px 0" }}
|
style={{margin: "20px 0"}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
|
||||||
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
form={commentForm}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="comment"
|
|
||||||
rules={[{ required: true, message: "Напишите комментарий" }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Напишите комментарий к решению..."
|
placeholder="Напишите комментарий к решению..."
|
||||||
allowClear
|
allowClear
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
style={{ marginTop: 20, marginBottom: 20}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item style={{ marginBottom: 0 }}>
|
<Button onClick={() => onCommentSubmit(solution.id)} type="primary"
|
||||||
<Button type="primary" htmlType="submit">
|
htmlType="submit">
|
||||||
Отправить комментарий
|
Отправить комментарий
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
))}
|
))}
|
||||||
@ -447,7 +443,7 @@ const ViewTaskModal = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<div style={{ marginTop: 32 }}>
|
<div style={{marginTop: 32}}>
|
||||||
<Title level={4}>Комментарии к решению</Title>
|
<Title level={4}>Комментарии к решению</Title>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -464,10 +460,13 @@ const ViewTaskModal = () => {
|
|||||||
<List
|
<List
|
||||||
dataSource={solution.solution_comments}
|
dataSource={solution.solution_comments}
|
||||||
renderItem={(comment) => (
|
renderItem={(comment) => (
|
||||||
<List.Item style={{ padding: "12px 16px", borderBottom: "1px solid #f0f0f0" }}>
|
<List.Item style={{
|
||||||
|
padding: "12px 16px",
|
||||||
|
borderBottom: "1px solid #f0f0f0"
|
||||||
|
}}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar style={{ backgroundColor: "#1890ff" }}>
|
<Avatar style={{backgroundColor: "#1890ff"}}>
|
||||||
{comment.comment_autor.first_name[0]}
|
{comment.comment_autor.first_name[0]}
|
||||||
{comment.comment_autor.last_name[0]}
|
{comment.comment_autor.last_name[0]}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@ -478,12 +477,13 @@ const ViewTaskModal = () => {
|
|||||||
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
||||||
</Text>
|
</Text>
|
||||||
{comment.comment_autor.role?.title === "teacher" && (
|
{comment.comment_autor.role?.title === "teacher" && (
|
||||||
<Tag color="gold" size="small">Преподаватель</Tag>
|
<Tag color="gold"
|
||||||
|
size="small">Преподаватель</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{fontSize: 12}}>
|
||||||
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@ -495,7 +495,7 @@ const ViewTaskModal = () => {
|
|||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
|
dangerouslySetInnerHTML={{__html: comment.comment_text}}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
@ -504,33 +504,25 @@ const ViewTaskModal = () => {
|
|||||||
<Empty
|
<Empty
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
description="Пока нет комментариев"
|
description="Пока нет комментариев"
|
||||||
style={{ margin: "20px 0" }}
|
style={{margin: "20px 0"}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
|
||||||
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
form={commentForm}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="comment"
|
|
||||||
rules={[{ required: true, message: "Напишите комментарий" }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Напишите комментарий к решению..."
|
placeholder="Напишите комментарий к решению..."
|
||||||
allowClear
|
allowClear
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
style={{ marginTop: 20, marginBottom: 20}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item style={{ marginBottom: 0 }}>
|
<Button onClick={() => onCommentSubmit(solution.id)} type="primary"
|
||||||
<Button type="primary" htmlType="submit">
|
htmlType="submit">
|
||||||
Отправить комментарий
|
Отправить комментарий
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const useViewTaskModal = () => {
|
|||||||
} = useSelector((state) => state.tasks);
|
} = useSelector((state) => state.tasks);
|
||||||
|
|
||||||
const [assessmentForm] = Form.useForm();
|
const [assessmentForm] = Form.useForm();
|
||||||
|
const [comment, setComment] = useState("");
|
||||||
|
|
||||||
const [
|
const [
|
||||||
createSolution,
|
createSolution,
|
||||||
@ -43,34 +44,35 @@ const useViewTaskModal = () => {
|
|||||||
isError: isErrorCreatingComment
|
isError: isErrorCreatingComment
|
||||||
}] = useCreateCommentMutation();
|
}] = useCreateCommentMutation();
|
||||||
|
|
||||||
const onCommentSubmit = async (solutionId, commentText) => {
|
const onCommentSubmit = async (solutionId) => {
|
||||||
if (!commentText?.trim()) return;
|
if (!comment?.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createComment({
|
await createComment({
|
||||||
solutionId: solutionId,
|
solutionId: solutionId,
|
||||||
comment: {
|
comment: {
|
||||||
comment_text: commentText.trim()
|
comment_text: comment.trim()
|
||||||
}
|
}
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
commentForm.resetFields();
|
setComment("");
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "Комментарий отправлен",
|
title: "Комментарий отправлен",
|
||||||
description: "Ваш комментарий успешно добавлен",
|
description: "Ваш комментарий успешно добавлен",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка",
|
title: "Ошибка",
|
||||||
description: error?.data?.detail || "Не удалось отправить комментарий",
|
description: error?.data?.detail || "Не удалось отправить комментарий",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleAddFile = (file) => {
|
const handleAddFile = (file) => {
|
||||||
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
|
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
|
||||||
if (file.size > maxSize) {
|
if (file.size > maxSize) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка вставки",
|
title: "Ошибка вставки",
|
||||||
description: "Файл слишком большой.",
|
description: "Файл слишком большой.",
|
||||||
placement: "topRight",
|
placement: "topRight",
|
||||||
});
|
});
|
||||||
@ -387,6 +389,8 @@ const useViewTaskModal = () => {
|
|||||||
onAssessmentFinish,
|
onAssessmentFinish,
|
||||||
assessmentForm,
|
assessmentForm,
|
||||||
onCommentSubmit,
|
onCommentSubmit,
|
||||||
|
setComment,
|
||||||
|
comment,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -156,7 +156,7 @@ const CoursesPage = () => {
|
|||||||
<Divider/>
|
<Divider/>
|
||||||
<Text type="secondary">Прогресс:</Text>
|
<Text type="secondary">Прогресс:</Text>
|
||||||
{courseProgress[course.id] !== undefined && (
|
{courseProgress[course.id] !== undefined && (
|
||||||
<Progress percent={courseProgress[course.id]} size={[300, 20]}/>
|
<Progress showInfo={true} status={"active"} percent={courseProgress[course.id]} size={[300, 20]}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -166,6 +166,7 @@ const CoursesPage = () => {
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(isTeacher || isAdmin) && (
|
||||||
<Tooltip title="Создать курс">
|
<Tooltip title="Создать курс">
|
||||||
<FloatButton
|
<FloatButton
|
||||||
icon={<PlusOutlined/>}
|
icon={<PlusOutlined/>}
|
||||||
@ -173,6 +174,7 @@ const CoursesPage = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<CreateCourseModalForm/>
|
<CreateCourseModalForm/>
|
||||||
<UpdateCourseModalForm/>
|
<UpdateCourseModalForm/>
|
||||||
|
|||||||
@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: '../dist',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user