исправил различные функции

This commit is contained in:
Андрей Дувакин 2025-11-30 09:53:59 +05:00
parent 7bd89686a9
commit f4e3cbf3be
23 changed files with 378 additions and 75 deletions

4
api/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.venv
k8s
.idea
.env

18
api/app/Dockerfile Normal file
View 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"]

View File

@ -42,6 +42,7 @@ class SolutionCommentCreate(SolutionCommentBase):
class SolutionCommentRead(SolutionCommentBase):
comment_autor_id: int
solution_id: int
created_at: datetime
comment_autor: UserRead

View File

@ -20,7 +20,7 @@ def start_app():
api_app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_origins=['https://api.lectio.numerum.team', 'https://lectio.numerum.team', 'http://localhost:5173'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: lectio-api-app
description: Lectio API project
version: 0.1.0
appVersion: "1.0"

View 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"

View 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 }}

View 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 }}

View 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 }}

View 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

View File

@ -8,3 +8,5 @@ pyjwt==2.9.0
fastapi==0.115.0
pydantic[email]==2.11.4
aiofiles==25.1.0
uvicorn==0.34.0
python-multipart==0.0.12

7
web/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
npm-debug.log
build
.dockerignore
.git
k8s
.env

27
web/Dockerfile Normal file
View 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"]

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: lectio-web-app
description: Lectio WEB project
version: 0.1.0
appVersion: "1.0"

View 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 }}

View 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 }}

View 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 }}

View 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

View File

@ -8,6 +8,7 @@ import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
import CourseDetailPage from "../Components/Pages/CourseDetailPage/CourseDetailPage.jsx";
import CoursesPage from "../Components/Pages/CoursesPage/CoursesPage.jsx";
import GradebookPage from "../Components/Pages/GradebookPage/GradebookPage.jsx";
const AppRouter = () => (
@ -20,6 +21,7 @@ const AppRouter = () => (
<Route path={"/courses"} element={<CoursesPage/>}/>
<Route path={"/profile"} element={<ProfilePage/>}/>
<Route path="/courses/:courseId" element={<CourseDetailPage />} />
<Route path="/courses/:courseId/gradebook" element={<GradebookPage />} />
<Route path={"*"} element={<Navigate to={"/courses"}/>}/>
</Route>
</Route>

View File

@ -52,7 +52,9 @@ const ViewTaskModal = () => {
onAssessmentFinish,
assessmentForm,
onCommentSubmit,
commentForm
commentForm,
setComment,
comment,
} = useViewTaskModal();
return (
@ -230,7 +232,7 @@ const ViewTaskModal = () => {
) : (
<Text type="secondary">Файлы не прикреплены</Text>
)}
<div style={{ marginTop: 32 }}>
<div style={{marginTop: 32}}>
<Title level={4}>Комментарии к решению</Title>
<div
@ -247,10 +249,13 @@ const ViewTaskModal = () => {
<List
dataSource={solution.solution_comments}
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
avatar={
<Avatar style={{ backgroundColor: "#1890ff" }}>
<Avatar style={{backgroundColor: "#1890ff"}}>
{comment.comment_autor.first_name[0]}
{comment.comment_autor.last_name[0]}
</Avatar>
@ -261,12 +266,13 @@ const ViewTaskModal = () => {
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
</Text>
{comment.comment_autor.role?.title === "teacher" && (
<Tag color="gold" size="small">Преподаватель</Tag>
<Tag color="gold"
size="small">Преподаватель</Tag>
)}
</Space>
}
description={
<Text type="secondary" style={{ fontSize: 12 }}>
<Text type="secondary" style={{fontSize: 12}}>
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
</Text>
}
@ -278,7 +284,7 @@ const ViewTaskModal = () => {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
dangerouslySetInnerHTML={{__html: comment.comment_text}}
/>
</List.Item>
)}
@ -287,33 +293,23 @@ const ViewTaskModal = () => {
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="Пока нет комментариев"
style={{ margin: "20px 0" }}
style={{margin: "20px 0"}}
/>
)}
</div>
<Form
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
style={{ marginTop: 16 }}
form={commentForm}
>
<Form.Item
name="comment"
rules={[{ required: true, message: "Напишите комментарий" }]}
>
<Input.TextArea
rows={3}
placeholder="Напишите комментарий к решению..."
allowClear
value={comment}
onChange={(e) => setComment(e.target.value)}
style={{ marginTop: 20, marginBottom: 20}}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button type="primary" htmlType="submit">
<Button onClick={() => onCommentSubmit(solution.id)} type="primary"
htmlType="submit">
Отправить комментарий
</Button>
</Form.Item>
</Form>
</div>
</Panel>
))}
@ -447,7 +443,7 @@ const ViewTaskModal = () => {
</Button>
</Form.Item>
</Form>
<div style={{ marginTop: 32 }}>
<div style={{marginTop: 32}}>
<Title level={4}>Комментарии к решению</Title>
<div
@ -464,10 +460,13 @@ const ViewTaskModal = () => {
<List
dataSource={solution.solution_comments}
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
avatar={
<Avatar style={{ backgroundColor: "#1890ff" }}>
<Avatar style={{backgroundColor: "#1890ff"}}>
{comment.comment_autor.first_name[0]}
{comment.comment_autor.last_name[0]}
</Avatar>
@ -478,12 +477,13 @@ const ViewTaskModal = () => {
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
</Text>
{comment.comment_autor.role?.title === "teacher" && (
<Tag color="gold" size="small">Преподаватель</Tag>
<Tag color="gold"
size="small">Преподаватель</Tag>
)}
</Space>
}
description={
<Text type="secondary" style={{ fontSize: 12 }}>
<Text type="secondary" style={{fontSize: 12}}>
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
</Text>
}
@ -495,7 +495,7 @@ const ViewTaskModal = () => {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
dangerouslySetInnerHTML={{__html: comment.comment_text}}
/>
</List.Item>
)}
@ -504,33 +504,25 @@ const ViewTaskModal = () => {
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="Пока нет комментариев"
style={{ margin: "20px 0" }}
style={{margin: "20px 0"}}
/>
)}
</div>
<Form
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
style={{ marginTop: 16 }}
form={commentForm}
>
<Form.Item
name="comment"
rules={[{ required: true, message: "Напишите комментарий" }]}
>
<Input.TextArea
rows={3}
placeholder="Напишите комментарий к решению..."
allowClear
value={comment}
onChange={(e) => setComment(e.target.value)}
style={{ marginTop: 20, marginBottom: 20}}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button type="primary" htmlType="submit">
<Button onClick={() => onCommentSubmit(solution.id)} type="primary"
htmlType="submit">
Отправить комментарий
</Button>
</Form.Item>
</Form>
</div>
</Panel>
))}

View File

@ -23,6 +23,7 @@ const useViewTaskModal = () => {
} = useSelector((state) => state.tasks);
const [assessmentForm] = Form.useForm();
const [comment, setComment] = useState("");
const [
createSolution,
@ -43,34 +44,35 @@ const useViewTaskModal = () => {
isError: isErrorCreatingComment
}] = useCreateCommentMutation();
const onCommentSubmit = async (solutionId, commentText) => {
if (!commentText?.trim()) return;
const onCommentSubmit = async (solutionId) => {
if (!comment?.trim()) return;
try {
await createComment({
solutionId: solutionId,
comment: {
comment_text: commentText.trim()
comment_text: comment.trim()
}
}).unwrap();
commentForm.resetFields();
setComment("");
notification.success({
message: "Комментарий отправлен",
title: "Комментарий отправлен",
description: "Ваш комментарий успешно добавлен",
});
} catch (error) {
notification.error({
message: "Ошибка",
title: "Ошибка",
description: error?.data?.detail || "Не удалось отправить комментарий",
});
}
};
const handleAddFile = (file) => {
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
if (file.size > maxSize) {
notification.error({
message: "Ошибка вставки",
title: "Ошибка вставки",
description: "Файл слишком большой.",
placement: "topRight",
});
@ -387,6 +389,8 @@ const useViewTaskModal = () => {
onAssessmentFinish,
assessmentForm,
onCommentSubmit,
setComment,
comment,
}
};

View File

@ -156,7 +156,7 @@ const CoursesPage = () => {
<Divider/>
<Text type="secondary">Прогресс:</Text>
{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>
)}
@ -166,6 +166,7 @@ const CoursesPage = () => {
</Row>
)}
{(isTeacher || isAdmin) && (
<Tooltip title="Создать курс">
<FloatButton
icon={<PlusOutlined/>}
@ -173,6 +174,7 @@ const CoursesPage = () => {
type="primary"
/>
</Tooltip>
)}
<CreateCourseModalForm/>
<UpdateCourseModalForm/>

View File

@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
build: {
outDir: '../dist',
},
})