сделал отображение прогресса
This commit is contained in:
parent
be01b5fc22
commit
c6a44e8185
@ -1,10 +1,10 @@
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, func, distinct
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.domain.models import Solution
|
from app.domain.models import Solution, Task
|
||||||
|
|
||||||
|
|
||||||
class SolutionsRepository:
|
class SolutionsRepository:
|
||||||
@ -19,6 +19,22 @@ class SolutionsRepository:
|
|||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
return result.scalars().first()
|
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]]:
|
async def get_by_task_id(self, task_id: int) -> Optional[List[Solution]]:
|
||||||
query = (
|
query = (
|
||||||
select(Solution)
|
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 typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Response
|
from fastapi import APIRouter, Depends, status, Response
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_db
|
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.domain.models import User
|
||||||
from app.infrastructure.dependencies import require_auth_user, require_admin
|
from app.infrastructure.dependencies import require_auth_user, require_admin
|
||||||
from app.infrastructure.register_service import RegisterService
|
from app.infrastructure.register_service import RegisterService
|
||||||
|
from app.infrastructure.user_check_lessons_service import UserCheckLessonsService
|
||||||
from app.infrastructure.users_service import UsersService
|
from app.infrastructure.users_service import UsersService
|
||||||
|
|
||||||
users_router = APIRouter()
|
users_router = APIRouter()
|
||||||
@ -101,3 +102,47 @@ async def get_users_by_role_name(
|
|||||||
):
|
):
|
||||||
users_service = UsersService(db)
|
users_service = UsersService(db)
|
||||||
return await users_service.get_by_role_name(role_name)
|
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:
|
class Config:
|
||||||
from_attributes = True
|
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.statuses import Status
|
||||||
from app.domain.models.task_files import TaskFile
|
from app.domain.models.task_files import TaskFile
|
||||||
from app.domain.models.tasks import Task
|
from app.domain.models.tasks import Task
|
||||||
|
from app.domain.models.user_check_lessons import UserCheckLessons
|
||||||
from app.domain.models.users import User
|
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')
|
creator: Mapped['User'] = relationship('User', back_populates='created_lessons', lazy='joined')
|
||||||
|
|
||||||
files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson')
|
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',
|
'SolutionComment',
|
||||||
back_populates='comment_autor',
|
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):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, 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"],
|
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,
|
useUpdateUserPasswordMutation,
|
||||||
useCreateUserMutation,
|
useCreateUserMutation,
|
||||||
useGetUsersByRoleNameQuery,
|
useGetUsersByRoleNameQuery,
|
||||||
|
useGetReadedLessonsByCourseQuery,
|
||||||
|
useSetLessonAsReadedMutation,
|
||||||
|
useGetMyCourseProgressQuery,
|
||||||
} = usersApi;
|
} = usersApi;
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
} from "antd";
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
BookOutlined,
|
BookOutlined, CheckCircleFilled, ClockCircleOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FormOutlined,
|
FormOutlined,
|
||||||
@ -55,6 +55,7 @@ const CourseDetailPage = () => {
|
|||||||
handleOpenTask,
|
handleOpenTask,
|
||||||
handleEditTask,
|
handleEditTask,
|
||||||
handleDeleteTask,
|
handleDeleteTask,
|
||||||
|
listReadLessonsIds,
|
||||||
} = useCourseDetailPage(courseId);
|
} = useCourseDetailPage(courseId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -177,11 +178,30 @@ const CourseDetailPage = () => {
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
<Text>
|
<Text>
|
||||||
{isLesson ? "Лекционный материал" : "Задание"}
|
{isLesson ? "Лекционный материал" : "Задание"}
|
||||||
</Text>
|
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
|
|||||||
@ -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 {useGetCourseByIdQuery} from "../../../Api/coursesApi.js";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import {useDispatch} from "react-redux";
|
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 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))
|
dispatch(setSelectedLessonToView(lesson))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,6 +148,15 @@ const useCourseDetailPage = (courseId) => {
|
|||||||
dispatch(setSelectedTaskToUpdate(task))
|
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 {
|
return {
|
||||||
tasksData,
|
tasksData,
|
||||||
isTeacherOrAdmin,
|
isTeacherOrAdmin,
|
||||||
@ -143,6 +173,7 @@ const useCourseDetailPage = (courseId) => {
|
|||||||
handleOpenTask,
|
handleOpenTask,
|
||||||
handleEditTask,
|
handleEditTask,
|
||||||
handleDeleteTask,
|
handleDeleteTask,
|
||||||
|
listReadLessonsIds,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar, Result, FloatButton, Tooltip,
|
Avatar, Result, FloatButton, Tooltip, Progress, Divider,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@ -20,6 +20,8 @@ import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.js
|
|||||||
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
|
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
|
||||||
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
|
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import CONFIG from "../../../Core/сonfig.js";
|
||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
@ -35,6 +37,38 @@ const CoursesPage = () => {
|
|||||||
openEditModal,
|
openEditModal,
|
||||||
} = useCoursesPage();
|
} = 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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<LoadingIndicator/>
|
<LoadingIndicator/>
|
||||||
@ -119,6 +153,11 @@ const CoursesPage = () => {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
))}
|
))}
|
||||||
</Avatar.Group>
|
</Avatar.Group>
|
||||||
|
<Divider/>
|
||||||
|
<Text type="secondary">Прогресс:</Text>
|
||||||
|
{courseProgress[course.id] !== undefined && (
|
||||||
|
<Progress percent={courseProgress[course.id]} size={[300, 20]}/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</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 {useGetAllCoursesQuery, useGetAllMyCoursesQuery} from "../../../Api/coursesApi.js";
|
||||||
import CONFIG from "../../../Core/сonfig.js";
|
import CONFIG from "../../../Core/сonfig.js";
|
||||||
import {ROLES} from "../../../Core/constants.js";
|
import {ROLES} from "../../../Core/constants.js";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user