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

1from collections.abc import Iterable 

2from uuid import UUID 

3 

4from sqlalchemy.orm import Session 

5 

6from app.models import Course, User 

7from app.schemas import CourseCreate, CourseUpdate 

8 

9 

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() 

13 

14 

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() 

18 

19 

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 

27 

28 

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 

34 

35 update_data = course_update.model_dump(exclude_unset=True) 

36 for field, value in update_data.items(): 

37 setattr(db_course, field, value) 

38 

39 db.commit() 

40 db.refresh(db_course) 

41 return db_course 

42 

43 

44def delete_course(db: Session, course_id: UUID) -> bool: 

45 """Delete a course. 

46 

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 

55 

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 ) 

61 

62 db.delete(db_course) 

63 db.commit() 

64 return True 

65 

66 

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. 

74 

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() 

78 

79 

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. 

86 

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 [] 

96 

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() 

102 

103 

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``. 

110 

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