сделал отображение прогресса
This commit is contained in:
parent
be01b5fc22
commit
c6a44e8185
@ -1,10 +1,10 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, func, distinct
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.domain.models import Solution
|
||||
from app.domain.models import Solution, Task
|
||||
|
||||
|
||||
class SolutionsRepository:
|
||||
@ -19,6 +19,22 @@ class SolutionsRepository:
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_completed_tasks_by_course_and_user(
|
||||
self,
|
||||
course_id: int,
|
||||
user_id: int
|
||||
) -> List[int]:
|
||||
query = (
|
||||
select(distinct(Solution.task_id))
|
||||
.join(Task, Solution.task_id == Task.id)
|
||||
.where(
|
||||
Solution.student_id == user_id,
|
||||
Task.course_id == course_id
|
||||
)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_task_id(self, task_id: int) -> Optional[List[Solution]]:
|
||||
query = (
|
||||
select(Solution)
|
||||
|
||||
36
api/app/application/user_check_lessons_repository.py
Normal file
36
api/app/application/user_check_lessons_repository.py
Normal file
@ -0,0 +1,36 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import Lesson, UserCheckLessons
|
||||
|
||||
|
||||
class UserCheckLessonsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_course_id_and_user_id(self, course_id: int, user_id: int) -> Optional[List[UserCheckLessons]]:
|
||||
query = (
|
||||
select(UserCheckLessons)
|
||||
.join(Lesson, UserCheckLessons.lesson_id == Lesson.id) # связь с лекцией
|
||||
.where(
|
||||
Lesson.course_id == course_id,
|
||||
UserCheckLessons.user_id == user_id
|
||||
)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_user_id_and_lesson_id(self, user_id: int, lesson_id: int) -> Optional[UserCheckLessons]:
|
||||
query = (
|
||||
select(UserCheckLessons)
|
||||
.filter_by(user_id=user_id, lesson_id=lesson_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def create(self, user_check_lessons: UserCheckLessons) -> UserCheckLessons:
|
||||
self.db.add(user_check_lessons)
|
||||
await self.db.commit()
|
||||
return user_check_lessons
|
||||
@ -1,13 +1,14 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from fastapi import APIRouter, Depends, status, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.session import get_db
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest, UserCreate
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest, UserCreate, UserCheckLessonRead
|
||||
from app.domain.models import User
|
||||
from app.infrastructure.dependencies import require_auth_user, require_admin
|
||||
from app.infrastructure.register_service import RegisterService
|
||||
from app.infrastructure.user_check_lessons_service import UserCheckLessonsService
|
||||
from app.infrastructure.users_service import UsersService
|
||||
|
||||
users_router = APIRouter()
|
||||
@ -101,3 +102,47 @@ async def get_users_by_role_name(
|
||||
):
|
||||
users_service = UsersService(db)
|
||||
return await users_service.get_by_role_name(role_name)
|
||||
|
||||
|
||||
@users_router.get(
|
||||
'/check-my-lessons/{course_id}/',
|
||||
response_model=Optional[List[UserCheckLessonRead]],
|
||||
summary='Return all users with given lessons',
|
||||
description='Return all users with given lessons',
|
||||
)
|
||||
async def get_all_users_in_lessons(
|
||||
course_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user)
|
||||
):
|
||||
user_check_lessons_service = UserCheckLessonsService(db)
|
||||
return await user_check_lessons_service.get_by_course_id_and_user_id(course_id, user.id)
|
||||
|
||||
|
||||
@users_router.post(
|
||||
'/check-lesson/{lesson_id}/',
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary='Mark lesson as checked',
|
||||
description='Mark lesson as checked',
|
||||
)
|
||||
async def mark_lesson_checked(
|
||||
lesson_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user)
|
||||
):
|
||||
user_check_lessons_service = UserCheckLessonsService(db)
|
||||
await user_check_lessons_service.create(lesson_id, user.id)
|
||||
|
||||
|
||||
@users_router.get(
|
||||
'/my-progress/{course_id}/',
|
||||
summary='Return user progress',
|
||||
description='Return user progress',
|
||||
)
|
||||
async def get_user_progress(
|
||||
course_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
user_check_lessons_service = UserCheckLessonsService(db)
|
||||
return await user_check_lessons_service.calculate_user_progress(course_id, user.id)
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
"""0004 добавил таблицу для отслеживания прохождения лекций
|
||||
|
||||
Revision ID: 5664875e4492
|
||||
Revises: f8fd9a27eaa7
|
||||
Create Date: 2025-11-29 19:49:20.397877
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5664875e4492'
|
||||
down_revision: Union[str, Sequence[str], None] = 'f8fd9a27eaa7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user_check_lessons',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('lesson_id', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['lesson_id'], ['public.lessons.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='public'
|
||||
)
|
||||
op.drop_constraint(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', type_='foreignkey')
|
||||
op.drop_constraint(op.f('course_teachers_course_id_fkey'), 'course_teachers', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'course_teachers', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'course_teachers', 'users', ['teacher_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('enrollments_student_id_fkey'), 'enrollments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('enrollments_course_id_fkey'), 'enrollments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'enrollments', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'enrollments', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'lesson_files', 'lessons', ['lesson_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('lessons_creator_id_fkey'), 'lessons', type_='foreignkey')
|
||||
op.drop_constraint(op.f('lessons_course_id_fkey'), 'lessons', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'lessons', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'lessons', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('solution_comments_solution_id_fkey'), 'solution_comments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('solution_comments_comment_autor_id_fkey'), 'solution_comments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'solution_comments', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'solution_comments', 'users', ['comment_autor_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('solution_files_solution_id_fkey'), 'solution_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'solution_files', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('solutions_task_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('solutions_assessment_autor_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('solutions_student_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'solutions', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'solutions', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'solutions', 'users', ['assessment_autor_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('task_files_task_id_fkey'), 'task_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'task_files', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('tasks_course_id_fkey'), 'tasks', type_='foreignkey')
|
||||
op.drop_constraint(op.f('tasks_creator_id_fkey'), 'tasks', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'tasks', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'tasks', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('users_role_id_fkey'), 'users', type_='foreignkey')
|
||||
op.drop_constraint(op.f('users_status_id_fkey'), 'users', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'users', 'statuses', ['status_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('users_status_id_fkey'), 'users', 'statuses', ['status_id'], ['id'])
|
||||
op.create_foreign_key(op.f('users_role_id_fkey'), 'users', 'roles', ['role_id'], ['id'])
|
||||
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('tasks_creator_id_fkey'), 'tasks', 'users', ['creator_id'], ['id'])
|
||||
op.create_foreign_key(op.f('tasks_course_id_fkey'), 'tasks', 'courses', ['course_id'], ['id'])
|
||||
op.drop_constraint(None, 'task_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('task_files_task_id_fkey'), 'task_files', 'tasks', ['task_id'], ['id'])
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('solutions_student_id_fkey'), 'solutions', 'users', ['student_id'], ['id'])
|
||||
op.create_foreign_key(op.f('solutions_assessment_autor_id_fkey'), 'solutions', 'users', ['assessment_autor_id'], ['id'])
|
||||
op.create_foreign_key(op.f('solutions_task_id_fkey'), 'solutions', 'tasks', ['task_id'], ['id'])
|
||||
op.drop_constraint(None, 'solution_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('solution_files_solution_id_fkey'), 'solution_files', 'solutions', ['solution_id'], ['id'])
|
||||
op.drop_constraint(None, 'solution_comments', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'solution_comments', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('solution_comments_comment_autor_id_fkey'), 'solution_comments', 'users', ['comment_autor_id'], ['id'])
|
||||
op.create_foreign_key(op.f('solution_comments_solution_id_fkey'), 'solution_comments', 'solutions', ['solution_id'], ['id'])
|
||||
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('lessons_course_id_fkey'), 'lessons', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('lessons_creator_id_fkey'), 'lessons', 'users', ['creator_id'], ['id'])
|
||||
op.drop_constraint(None, 'lesson_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', 'lessons', ['lesson_id'], ['id'])
|
||||
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('enrollments_course_id_fkey'), 'enrollments', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('enrollments_student_id_fkey'), 'enrollments', 'users', ['student_id'], ['id'])
|
||||
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('course_teachers_course_id_fkey'), 'course_teachers', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', 'users', ['teacher_id'], ['id'])
|
||||
op.drop_table('user_check_lessons', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
@ -62,3 +62,19 @@ class UserRead(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserCheckLessonBase(BaseModel):
|
||||
lesson_id: int
|
||||
user_id: int
|
||||
|
||||
|
||||
class UserCheckLessonCreate(UserCheckLessonBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserCheckLessonRead(UserCheckLessonBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@ -21,4 +21,5 @@ from app.domain.models.solutions import Solution
|
||||
from app.domain.models.statuses import Status
|
||||
from app.domain.models.task_files import TaskFile
|
||||
from app.domain.models.tasks import Task
|
||||
from app.domain.models.user_check_lessons import UserCheckLessons
|
||||
from app.domain.models.users import User
|
||||
|
||||
@ -21,3 +21,4 @@ class Lesson(RootTable):
|
||||
creator: Mapped['User'] = relationship('User', back_populates='created_lessons', lazy='joined')
|
||||
|
||||
files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson')
|
||||
user_check_lessons: Mapped[List['UserCheckLessons']] = relationship('UserCheckLessons', back_populates='lesson')
|
||||
|
||||
14
api/app/domain/models/user_check_lessons.py
Normal file
14
api/app/domain/models/user_check_lessons.py
Normal file
@ -0,0 +1,14 @@
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.domain.models.base import RootTable
|
||||
|
||||
|
||||
class UserCheckLessons(RootTable):
|
||||
__tablename__ = 'user_check_lessons'
|
||||
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
|
||||
lesson_id: Mapped[int] = mapped_column(ForeignKey('lessons.id'))
|
||||
|
||||
user: Mapped['User'] = relationship('User', back_populates='user_check_lessons')
|
||||
lesson: Mapped['Lesson'] = relationship('Lesson', back_populates='user_check_lessons')
|
||||
@ -51,6 +51,11 @@ class User(PhotoAbstract):
|
||||
'SolutionComment',
|
||||
back_populates='comment_autor',
|
||||
)
|
||||
from app.domain.models.user_check_lessons import UserCheckLessons
|
||||
user_check_lessons: Mapped[List['UserCheckLessons']] = relationship(
|
||||
'UserCheckLessons',
|
||||
back_populates='user',
|
||||
)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
102
api/app/infrastructure/user_check_lessons_service.py
Normal file
102
api/app/infrastructure/user_check_lessons_service.py
Normal file
@ -0,0 +1,102 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.courses_repository import CoursesRepository
|
||||
from app.application.lessons_repository import LessonsRepository
|
||||
from app.application.solutions_repository import SolutionsRepository
|
||||
from app.application.tasks_repository import TasksRepository
|
||||
from app.application.user_check_lessons_repository import UserCheckLessonsRepository
|
||||
from app.application.users_repository import UsersRepository
|
||||
from app.domain.entities.users import UserCheckLessonRead, UserCheckLessonCreate
|
||||
from app.domain.models import UserCheckLessons
|
||||
|
||||
|
||||
class UserCheckLessonsService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.user_check_lessons_repository = UserCheckLessonsRepository(db)
|
||||
self.users_repository = UsersRepository(db)
|
||||
self.lessons_repository = LessonsRepository(db)
|
||||
self.courses_repository = CoursesRepository(db)
|
||||
self.tasks_repository = TasksRepository(db)
|
||||
self.lessons_repository = LessonsRepository(db)
|
||||
self.solutions_repository = SolutionsRepository(db)
|
||||
|
||||
async def get_by_course_id_and_user_id(self, course_id: int, user_id: int) -> Optional[List[UserCheckLessonRead]]:
|
||||
user = await self.users_repository.get_by_id(user_id)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
if course is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Курс не найден"
|
||||
)
|
||||
|
||||
checks = await self.user_check_lessons_repository.get_by_course_id_and_user_id(course_id, user_id)
|
||||
response = []
|
||||
for check in checks:
|
||||
response.append(
|
||||
UserCheckLessonRead.model_validate(check)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def create(self, lesson_id: int, user_id: int) -> None:
|
||||
user = await self.users_repository.get_by_id(user_id)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
lesson = await self.lessons_repository.get_by_id(lesson_id)
|
||||
if lesson is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Лекция не найдена"
|
||||
)
|
||||
|
||||
user_check = await self.user_check_lessons_repository.get_by_user_id_and_lesson_id(
|
||||
user_id,
|
||||
lesson_id
|
||||
)
|
||||
|
||||
if user_check is not None:
|
||||
return
|
||||
|
||||
user_check_model = UserCheckLessons(
|
||||
lesson_id=lesson_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
user_check_model = await self.user_check_lessons_repository.create(user_check_model)
|
||||
|
||||
async def calculate_user_progress(self, course_id: int, user_id: int) -> float:
|
||||
total_lessons = await self.lessons_repository.get_all_by_course(course_id)
|
||||
total_tasks = await self.tasks_repository.get_all_by_course(course_id)
|
||||
|
||||
total_content = len(total_lessons) + len(total_tasks)
|
||||
if total_content == 0:
|
||||
return 100.0
|
||||
|
||||
passed_lessons = await self.user_check_lessons_repository.get_by_course_id_and_user_id(
|
||||
course_id=course_id,
|
||||
user_id=user_id
|
||||
)
|
||||
passed_lessons_count = len(passed_lessons)
|
||||
|
||||
completed_tasks = await self.solutions_repository.get_completed_tasks_by_course_and_user(
|
||||
course_id=course_id,
|
||||
user_id=user_id
|
||||
)
|
||||
completed_tasks_count = len(completed_tasks)
|
||||
|
||||
completed_total = passed_lessons_count + completed_tasks_count
|
||||
progress = (completed_total / total_content) * 100
|
||||
|
||||
return round(progress, 2)
|
||||
@ -46,6 +46,27 @@ export const usersApi = createApi({
|
||||
}),
|
||||
providesTags: ["user"],
|
||||
}),
|
||||
getReadedLessonsByCourse: builder.query({
|
||||
query: (courseId) => ({
|
||||
url: `/users/check-my-lessons/${courseId}/`,
|
||||
method: "GET",
|
||||
}),
|
||||
providesTags: ["user"],
|
||||
}),
|
||||
setLessonAsReaded: builder.mutation({
|
||||
query: (lessonId) => ({
|
||||
url: `/users/check-lesson/${lessonId}/`,
|
||||
method: "POST",
|
||||
}),
|
||||
invalidatesTags: ["user"],
|
||||
}),
|
||||
getMyCourseProgress: builder.query({
|
||||
query: (courseId) => ({
|
||||
url: `/users/my-progress/${courseId}/`,
|
||||
method: "GET",
|
||||
}),
|
||||
providesTags: ["user"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -56,4 +77,7 @@ export const {
|
||||
useUpdateUserPasswordMutation,
|
||||
useCreateUserMutation,
|
||||
useGetUsersByRoleNameQuery,
|
||||
useGetReadedLessonsByCourseQuery,
|
||||
useSetLessonAsReadedMutation,
|
||||
useGetMyCourseProgressQuery,
|
||||
} = usersApi;
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
BookOutlined,
|
||||
BookOutlined, CheckCircleFilled, ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FormOutlined,
|
||||
@ -55,6 +55,7 @@ const CourseDetailPage = () => {
|
||||
handleOpenTask,
|
||||
handleEditTask,
|
||||
handleDeleteTask,
|
||||
listReadLessonsIds,
|
||||
} = useCourseDetailPage(courseId);
|
||||
|
||||
if (isLoading) {
|
||||
@ -177,10 +178,29 @@ const CourseDetailPage = () => {
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<Text>
|
||||
{isLesson ? "Лекционный материал" : "Задание"}
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
{isLesson ? "Лекционный материал" : "Задание"}
|
||||
</Text>
|
||||
{isLesson && (
|
||||
listReadLessonsIds.includes(item.id) ? (
|
||||
<Space style={{color: "#52c41a", fontWeight: 500}}>
|
||||
<CheckCircleFilled style={{fontSize: 18}}/>
|
||||
<span>Пройдено</span>
|
||||
</Space>
|
||||
) : (
|
||||
<Space style={{color: "#999"}}>
|
||||
<ClockCircleOutlined style={{fontSize: 16}}/>
|
||||
<span>Не прочитано</span>
|
||||
</Space>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||
import {
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
useGetReadedLessonsByCourseQuery,
|
||||
useSetLessonAsReadedMutation
|
||||
} from "../../../Api/usersApi.js";
|
||||
import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js";
|
||||
import {useEffect} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
@ -107,7 +111,24 @@ const useCourseDetailPage = (courseId) => {
|
||||
|
||||
const isTeacherOrAdmin = [CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData?.role?.title);
|
||||
|
||||
const handleOpenLesson = (lesson) => {
|
||||
const [
|
||||
markLessonAsRead
|
||||
] = useSetLessonAsReadedMutation();
|
||||
|
||||
const markLesson = async (lessonId) => {
|
||||
try {
|
||||
await markLessonAsRead(lessonId);
|
||||
} catch {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: "Не удалось отметить лекцию как прочитанную",
|
||||
placement: "topRight",
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenLesson = async (lesson) => {
|
||||
await markLesson(lesson.id)
|
||||
dispatch(setSelectedLessonToView(lesson))
|
||||
};
|
||||
|
||||
@ -127,6 +148,15 @@ const useCourseDetailPage = (courseId) => {
|
||||
dispatch(setSelectedTaskToUpdate(task))
|
||||
};
|
||||
|
||||
const {
|
||||
data: readedLessons = []
|
||||
} = useGetReadedLessonsByCourseQuery(courseData?.id, {
|
||||
pollingInterval: 10000,
|
||||
skip: courseData?.id === null || courseData?.id === undefined
|
||||
});
|
||||
|
||||
const listReadLessonsIds = readedLessons.map((data) => data.lesson_id);
|
||||
|
||||
return {
|
||||
tasksData,
|
||||
isTeacherOrAdmin,
|
||||
@ -143,6 +173,7 @@ const useCourseDetailPage = (courseId) => {
|
||||
handleOpenTask,
|
||||
handleEditTask,
|
||||
handleDeleteTask,
|
||||
listReadLessonsIds,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
Avatar, Result, FloatButton, Tooltip,
|
||||
Avatar, Result, FloatButton, Tooltip, Progress, Divider,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
@ -20,6 +20,8 @@ import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.js
|
||||
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
|
||||
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useEffect, useState} from "react";
|
||||
import CONFIG from "../../../Core/сonfig.js";
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
@ -35,6 +37,38 @@ const CoursesPage = () => {
|
||||
openEditModal,
|
||||
} = useCoursesPage();
|
||||
|
||||
|
||||
const [courseProgress, setCourseProgress] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (courses.length === 0) return;
|
||||
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) return;
|
||||
|
||||
const fetchProgress = async () => {
|
||||
const progress = {};
|
||||
for (const course of courses) {
|
||||
try {
|
||||
const res = await fetch(`${CONFIG.BASE_URL}/users/my-progress/${course.id}/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
progress[course.id] = data; // data — это число, например 87.5
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Progress fetch error for course", course.id);
|
||||
}
|
||||
}
|
||||
setCourseProgress(progress);
|
||||
};
|
||||
|
||||
fetchProgress();
|
||||
}, [courses]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingIndicator/>
|
||||
@ -119,6 +153,11 @@ const CoursesPage = () => {
|
||||
</Avatar>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<Divider/>
|
||||
<Text type="secondary">Прогресс:</Text>
|
||||
{courseProgress[course.id] !== undefined && (
|
||||
<Progress percent={courseProgress[course.id]} size={[300, 20]}/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetAuthenticatedUserDataQuery, useGetMyCourseProgressQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetAllCoursesQuery, useGetAllMyCoursesQuery} from "../../../Api/coursesApi.js";
|
||||
import CONFIG from "../../../Core/сonfig.js";
|
||||
import {ROLES} from "../../../Core/constants.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user