Coverage for app/crud/courses.py: 100.00%
49 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-25 15:51 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-25 15:51 +0000
1from collections.abc import Iterable
2from uuid import UUID
4from sqlalchemy.orm import Session
6from app.models import Course, User
7from app.schemas import CourseCreate, CourseUpdate
10def get_course(db: Session, course_id: UUID) -> Course | None:
11 """Get course by ID"""
12 return db.query(Course).filter(Course.courseId == course_id).first()
15def get_courses(db: Session, skip: int = 0, limit: int = 100) -> list[Course]:
16 """Get all courses"""
17 return db.query(Course).offset(skip).limit(limit).all()
20def create_course(db: Session, course: CourseCreate) -> Course:
21 """Create a new course"""
22 db_course = Course(name=course.name)
23 db.add(db_course)
24 db.commit()
25 db.refresh(db_course)
26 return db_course
29def update_course(db: Session, course_id: UUID, course_update: CourseUpdate) -> Course | None:
30 """Update course information"""
31 db_course = get_course(db, course_id)
32 if not db_course:
33 return None
35 update_data = course_update.model_dump(exclude_unset=True)
36 for field, value in update_data.items():
37 setattr(db_course, field, value)
39 db.commit()
40 db.refresh(db_course)
41 return db_course
44def delete_course(db: Session, course_id: UUID) -> bool:
45 """Delete a course.
47 The user-course link is a nullable FK on ``users.courseId`` and
48 deleting the course leaves enrolled users dangling. We unlink them
49 first so the deletion is idempotent and never trips the FK
50 constraint, regardless of how the schema is configured.
51 """
52 db_course = get_course(db, course_id)
53 if not db_course:
54 return False
56 # Detach members so the row deletion is allowed even if the FK
57 # is configured as ``ON DELETE RESTRICT``.
58 db.query(User).filter(User.courseId == course_id).update(
59 {User.courseId: None}, synchronize_session=False
60 )
62 db.delete(db_course)
63 db.commit()
64 return True
67# ----------------------------------------------------------------
68# MEMBER MANAGEMENT
69# ----------------------------------------------------------------
70# Members live as ``users.courseId`` on the ``User`` row, not in a
71# join table — a user is in at most one course at a time. The helpers
72# below treat that single FK as a small membership API so the router
73# stays slim.
75def get_course_members(db: Session, course_id: UUID) -> list[User]:
76 """Return users currently enrolled in ``course_id``."""
77 return db.query(User).filter(User.courseId == course_id).order_by(User.username).all()
80def add_users_to_course(
81 db: Session,
82 course_id: UUID,
83 user_ids: Iterable[UUID],
84) -> list[User]:
85 """Add a batch of users to a course.
87 Sets ``users.courseId`` for every existing user in ``user_ids``.
88 Missing user-ids are skipped silently — the caller already
89 validated the picker contents and there's no useful error to
90 surface for "this user vanished between picker and submit".
91 Returns the affected user rows post-update.
92 """
93 ids = [uid for uid in user_ids if uid is not None]
94 if not ids:
95 return []
97 db.query(User).filter(User.userId.in_(ids)).update(
98 {User.courseId: course_id}, synchronize_session=False
99 )
100 db.commit()
101 return db.query(User).filter(User.userId.in_(ids)).all()
104def remove_user_from_course(
105 db: Session,
106 course_id: UUID,
107 user_id: UUID,
108) -> bool:
109 """Remove ``user_id`` from ``course_id``.
111 Returns ``True`` if the user was actually a member of that course
112 and got detached, ``False`` if they weren't (so the router can
113 answer 404 cleanly). Detaching a user from a different course is
114 NOT done — that would silently mutate unrelated state.
115 """
116 user = db.query(User).filter(
117 User.userId == user_id,
118 User.courseId == course_id,
119 ).first()
120 if not user:
121 return False
122 user.courseId = None
123 db.commit()
124 return True