Skip to content

Commit b14a95d

Browse files
committed
Забытый файл из коммита 9ec6eb1
1 parent 9ec6eb1 commit b14a95d

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

courses/services/progress.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime
3+
4+
from django.db import IntegrityError, transaction
5+
from django.utils import timezone
6+
7+
from courses.models import (
8+
Course,
9+
CourseLesson,
10+
CourseModule,
11+
ProgressStatus,
12+
UserCourseProgress,
13+
UserLessonProgress,
14+
UserModuleProgress,
15+
)
16+
17+
18+
@dataclass(slots=True, frozen=True)
19+
class ProgressSnapshot:
20+
status: str
21+
percent: int
22+
23+
24+
def percent_from_counts(completed_count: int, total_count: int) -> int:
25+
if total_count <= 0:
26+
return 0
27+
if completed_count <= 0:
28+
return 0
29+
if completed_count >= total_count:
30+
return 100
31+
return int((completed_count * 100) / total_count)
32+
33+
34+
def status_from_percent(
35+
percent: int,
36+
*,
37+
allow_blocked: bool = False,
38+
blocked: bool = False,
39+
) -> str:
40+
if blocked and allow_blocked:
41+
return ProgressStatus.BLOCKED
42+
if percent <= 0:
43+
return ProgressStatus.NOT_STARTED
44+
if percent >= 100:
45+
return ProgressStatus.COMPLETED
46+
return ProgressStatus.IN_PROGRESS
47+
48+
49+
def build_progress_snapshot(
50+
completed_count: int,
51+
total_count: int,
52+
*,
53+
allow_blocked: bool = False,
54+
blocked: bool = False,
55+
) -> ProgressSnapshot:
56+
percent = percent_from_counts(completed_count, total_count)
57+
status = status_from_percent(percent, allow_blocked=allow_blocked, blocked=blocked)
58+
return ProgressSnapshot(status=status, percent=percent)
59+
60+
61+
def upsert_course_progress(
62+
user,
63+
course: Course,
64+
*,
65+
completed_lessons: int,
66+
total_lessons: int,
67+
touch_visit: bool = False,
68+
visited_at: datetime | None = None,
69+
) -> UserCourseProgress:
70+
snapshot = build_progress_snapshot(completed_lessons, total_lessons)
71+
manager = UserCourseProgress.objects
72+
if transaction.get_connection().in_atomic_block:
73+
manager = manager.select_for_update()
74+
progress = manager.filter(user=user, course=course).first()
75+
if progress is None:
76+
progress = UserCourseProgress(user=user, course=course)
77+
78+
progress.status = snapshot.status
79+
progress.percent = snapshot.percent
80+
if touch_visit:
81+
progress.last_visit_at = visited_at or timezone.now()
82+
try:
83+
progress.save(validate=False)
84+
except IntegrityError:
85+
# Concurrent create: retry as locked update.
86+
retry_manager = UserCourseProgress.objects
87+
if transaction.get_connection().in_atomic_block:
88+
retry_manager = retry_manager.select_for_update()
89+
progress = retry_manager.get(user=user, course=course)
90+
progress.status = snapshot.status
91+
progress.percent = snapshot.percent
92+
if touch_visit:
93+
progress.last_visit_at = visited_at or timezone.now()
94+
progress.save(validate=False)
95+
return progress
96+
97+
98+
def upsert_module_progress(
99+
user,
100+
module: CourseModule,
101+
*,
102+
completed_lessons: int,
103+
total_lessons: int,
104+
) -> UserModuleProgress:
105+
snapshot = build_progress_snapshot(completed_lessons, total_lessons)
106+
manager = UserModuleProgress.objects
107+
if transaction.get_connection().in_atomic_block:
108+
manager = manager.select_for_update()
109+
progress = manager.filter(user=user, module=module).first()
110+
if progress is None:
111+
progress = UserModuleProgress(user=user, module=module)
112+
113+
progress.status = snapshot.status
114+
progress.percent = snapshot.percent
115+
try:
116+
progress.save(validate=False)
117+
except IntegrityError:
118+
retry_manager = UserModuleProgress.objects
119+
if transaction.get_connection().in_atomic_block:
120+
retry_manager = retry_manager.select_for_update()
121+
progress = retry_manager.get(user=user, module=module)
122+
progress.status = snapshot.status
123+
progress.percent = snapshot.percent
124+
progress.save(validate=False)
125+
return progress
126+
127+
128+
def upsert_lesson_progress(
129+
user,
130+
lesson: CourseLesson,
131+
*,
132+
completed_tasks: int,
133+
total_tasks: int,
134+
current_task=None,
135+
blocked: bool = False,
136+
) -> UserLessonProgress:
137+
snapshot = build_progress_snapshot(
138+
completed_tasks,
139+
total_tasks,
140+
allow_blocked=True,
141+
blocked=blocked,
142+
)
143+
manager = UserLessonProgress.objects
144+
if transaction.get_connection().in_atomic_block:
145+
manager = manager.select_for_update()
146+
progress = manager.filter(user=user, lesson=lesson).first()
147+
if progress is None:
148+
progress = UserLessonProgress(user=user, lesson=lesson)
149+
150+
progress.status = snapshot.status
151+
progress.percent = snapshot.percent
152+
progress.current_task = current_task
153+
try:
154+
progress.save(validate=False)
155+
except IntegrityError:
156+
retry_manager = UserLessonProgress.objects
157+
if transaction.get_connection().in_atomic_block:
158+
retry_manager = retry_manager.select_for_update()
159+
progress = retry_manager.get(user=user, lesson=lesson)
160+
progress.status = snapshot.status
161+
progress.percent = snapshot.percent
162+
progress.current_task = current_task
163+
progress.save(validate=False)
164+
return progress
165+
166+
167+
@transaction.atomic
168+
def touch_course_visit(
169+
user,
170+
course: Course,
171+
*,
172+
visited_at: datetime | None = None,
173+
) -> UserCourseProgress:
174+
progress = UserCourseProgress.objects.select_for_update().filter(
175+
user=user,
176+
course=course,
177+
).first()
178+
if progress is None:
179+
progress = UserCourseProgress(user=user, course=course)
180+
progress.last_visit_at = visited_at or timezone.now()
181+
try:
182+
progress.save(validate=False)
183+
except IntegrityError:
184+
progress = UserCourseProgress.objects.select_for_update().get(
185+
user=user,
186+
course=course,
187+
)
188+
progress.last_visit_at = visited_at or timezone.now()
189+
progress.save(validate=False)
190+
return progress

0 commit comments

Comments
 (0)