diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..329ed4e --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,4 @@ +.venv +k8s +.idea +.env \ No newline at end of file diff --git a/api/app/Dockerfile b/api/app/Dockerfile new file mode 100644 index 0000000..1e6f0b5 --- /dev/null +++ b/api/app/Dockerfile @@ -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"] diff --git a/api/app/domain/entities/solutions.py b/api/app/domain/entities/solutions.py index e85e7e0..1969495 100644 --- a/api/app/domain/entities/solutions.py +++ b/api/app/domain/entities/solutions.py @@ -42,6 +42,7 @@ class SolutionCommentCreate(SolutionCommentBase): class SolutionCommentRead(SolutionCommentBase): comment_autor_id: int solution_id: int + created_at: datetime comment_autor: UserRead diff --git a/api/app/main.py b/api/app/main.py index 57b5862..47f738a 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -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=['*'], diff --git a/api/k8s/helm/lectio-api/Chart.yaml b/api/k8s/helm/lectio-api/Chart.yaml new file mode 100644 index 0000000..c372aa8 --- /dev/null +++ b/api/k8s/helm/lectio-api/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: lectio-api-app +description: Lectio API project +version: 0.1.0 +appVersion: "1.0" \ No newline at end of file diff --git a/api/k8s/helm/lectio-api/templates/deployment.yaml b/api/k8s/helm/lectio-api/templates/deployment.yaml new file mode 100644 index 0000000..f33cd56 --- /dev/null +++ b/api/k8s/helm/lectio-api/templates/deployment.yaml @@ -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" \ No newline at end of file diff --git a/api/k8s/helm/lectio-api/templates/ingress.yaml b/api/k8s/helm/lectio-api/templates/ingress.yaml new file mode 100644 index 0000000..f604998 --- /dev/null +++ b/api/k8s/helm/lectio-api/templates/ingress.yaml @@ -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 }} diff --git a/api/k8s/helm/lectio-api/templates/pvc.yaml b/api/k8s/helm/lectio-api/templates/pvc.yaml new file mode 100644 index 0000000..ed7b742 --- /dev/null +++ b/api/k8s/helm/lectio-api/templates/pvc.yaml @@ -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 }} \ No newline at end of file diff --git a/api/k8s/helm/lectio-api/templates/service.yaml b/api/k8s/helm/lectio-api/templates/service.yaml new file mode 100644 index 0000000..67b1eb9 --- /dev/null +++ b/api/k8s/helm/lectio-api/templates/service.yaml @@ -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 }} \ No newline at end of file diff --git a/api/k8s/helm/lectio-api/values.yaml b/api/k8s/helm/lectio-api/values.yaml new file mode 100644 index 0000000..e517f4e --- /dev/null +++ b/api/k8s/helm/lectio-api/values.yaml @@ -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 \ No newline at end of file diff --git a/api/req.txt b/api/req.txt index d3a8463..3a04e9a 100644 --- a/api/req.txt +++ b/api/req.txt @@ -7,4 +7,6 @@ werkzeug==3.1.3 pyjwt==2.9.0 fastapi==0.115.0 pydantic[email]==2.11.4 -aiofiles==25.1.0 \ No newline at end of file +aiofiles==25.1.0 +uvicorn==0.34.0 +python-multipart==0.0.12 \ No newline at end of file diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..593eedb --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +build +.dockerignore +.git +k8s +.env \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..388a3e3 --- /dev/null +++ b/web/Dockerfile @@ -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"] \ No newline at end of file diff --git a/web/k8s/helm/lectio-web/Chart.yaml b/web/k8s/helm/lectio-web/Chart.yaml new file mode 100644 index 0000000..c6af331 --- /dev/null +++ b/web/k8s/helm/lectio-web/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: lectio-web-app +description: Lectio WEB project +version: 0.1.0 +appVersion: "1.0" \ No newline at end of file diff --git a/web/k8s/helm/lectio-web/templates/deployment.yaml b/web/k8s/helm/lectio-web/templates/deployment.yaml new file mode 100644 index 0000000..3f5bd9c --- /dev/null +++ b/web/k8s/helm/lectio-web/templates/deployment.yaml @@ -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 }} \ No newline at end of file diff --git a/web/k8s/helm/lectio-web/templates/ingress.yaml b/web/k8s/helm/lectio-web/templates/ingress.yaml new file mode 100644 index 0000000..da2c7ec --- /dev/null +++ b/web/k8s/helm/lectio-web/templates/ingress.yaml @@ -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 }} diff --git a/web/k8s/helm/lectio-web/templates/service.yaml b/web/k8s/helm/lectio-web/templates/service.yaml new file mode 100644 index 0000000..67b1eb9 --- /dev/null +++ b/web/k8s/helm/lectio-web/templates/service.yaml @@ -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 }} \ No newline at end of file diff --git a/web/k8s/helm/lectio-web/values.yaml b/web/k8s/helm/lectio-web/values.yaml new file mode 100644 index 0000000..7d32b3a --- /dev/null +++ b/web/k8s/helm/lectio-web/values.yaml @@ -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 diff --git a/web/src/App/AppRouter.jsx b/web/src/App/AppRouter.jsx index 8237c7e..c89be54 100644 --- a/web/src/App/AppRouter.jsx +++ b/web/src/App/AppRouter.jsx @@ -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 = () => ( }/> }/> } /> + } /> }/> diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx index 545949c..a4a2180 100644 --- a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx +++ b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx @@ -52,7 +52,9 @@ const ViewTaskModal = () => { onAssessmentFinish, assessmentForm, onCommentSubmit, - commentForm + commentForm, + setComment, + comment, } = useViewTaskModal(); return ( @@ -230,7 +232,7 @@ const ViewTaskModal = () => { ) : ( Файлы не прикреплены )} -
+
Комментарии к решению
{ ( - + + {comment.comment_autor.first_name[0]} {comment.comment_autor.last_name[0]} @@ -261,12 +266,13 @@ const ViewTaskModal = () => { {comment.comment_autor.first_name} {comment.comment_autor.last_name} {comment.comment_autor.role?.title === "teacher" && ( - Преподаватель + Преподаватель )} } description={ - + {new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")} } @@ -278,7 +284,7 @@ const ViewTaskModal = () => { whiteSpace: "pre-wrap", wordBreak: "break-word", }} - dangerouslySetInnerHTML={{ __html: comment.comment_text}} + dangerouslySetInnerHTML={{__html: comment.comment_text}} /> )} @@ -287,33 +293,23 @@ const ViewTaskModal = () => { )}
+ setComment(e.target.value)} + style={{ marginTop: 20, marginBottom: 20}} + /> -
onCommentSubmit(solution.id, values.comment)} - style={{ marginTop: 16 }} - form={commentForm} - > - - - - - - - -
+
))} @@ -447,7 +443,7 @@ const ViewTaskModal = () => { -
+
Комментарии к решению
{ ( - + + {comment.comment_autor.first_name[0]} {comment.comment_autor.last_name[0]} @@ -478,12 +477,13 @@ const ViewTaskModal = () => { {comment.comment_autor.first_name} {comment.comment_autor.last_name} {comment.comment_autor.role?.title === "teacher" && ( - Преподаватель + Преподаватель )} } description={ - + {new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")} } @@ -495,7 +495,7 @@ const ViewTaskModal = () => { whiteSpace: "pre-wrap", wordBreak: "break-word", }} - dangerouslySetInnerHTML={{ __html: comment.comment_text}} + dangerouslySetInnerHTML={{__html: comment.comment_text}} /> )} @@ -504,33 +504,25 @@ const ViewTaskModal = () => { )}
-
onCommentSubmit(solution.id, values.comment)} - style={{ marginTop: 16 }} - form={commentForm} - > - - - - - - -
+ setComment(e.target.value)} + style={{ marginTop: 20, marginBottom: 20}} + /> + +
))} diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js index 40e18a2..42ac609 100644 --- a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js +++ b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js @@ -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, } }; diff --git a/web/src/Components/Pages/CoursesPage/CoursesPage.jsx b/web/src/Components/Pages/CoursesPage/CoursesPage.jsx index ce02fd4..172271e 100644 --- a/web/src/Components/Pages/CoursesPage/CoursesPage.jsx +++ b/web/src/Components/Pages/CoursesPage/CoursesPage.jsx @@ -156,7 +156,7 @@ const CoursesPage = () => { Прогресс: {courseProgress[course.id] !== undefined && ( - + )}
)} @@ -166,13 +166,15 @@ const CoursesPage = () => { )} - - } - onClick={openCreateModal} - type="primary" - /> - + {(isTeacher || isAdmin) && ( + + } + onClick={openCreateModal} + type="primary" + /> + + )} diff --git a/web/vite.config.js b/web/vite.config.js index 8b0f57b..52ef880 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + build: { + outDir: '../dist', + }, })