Update project
This commit is contained in:
@@ -5,6 +5,7 @@ content/receivers.py صدا زده میشن (نه مستقیم از views.py)
|
||||
یا UserLessonCompletion ساخته بشه، وضعیت Node هم خودکار بهروز بمونه —
|
||||
دقیقاً همون الگوی signal-based که برای discount/subscription استفاده شده.
|
||||
"""
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from statuslog.signals import status_changed
|
||||
@@ -13,7 +14,28 @@ from .models import Node, NodeProgress, UserLessonCompletion
|
||||
|
||||
|
||||
def _get_or_create_progress(user_id, node):
|
||||
return NodeProgress.objects.get_or_create(user_id=user_id, node=node)
|
||||
"""
|
||||
⚛️🔒 اصلاح race condition: قبلاً این تابع فقط get_or_create میکرد و
|
||||
ردیفِ برگرداندهشده (چه تازهساختهشده، چه از قبل موجود) هیچوقت قفل
|
||||
نمیشد. دو درخواستِ همزمان برای همون (user_id, node) — مثلاً دو پاسخِ
|
||||
پشتسرهم یا یک پاسخ و یک تکمیلِ درس که تقریباً همزمان میرسن —
|
||||
میتونستن هر دو همون ردیفِ NodeProgress رو با status/started_at/
|
||||
last_activity_at یکسان بخونن، هر دو مستقل مقدارهای جدید رو محاسبه
|
||||
کنن و save کنن؛ نتیجهی یکی از دو نویسش گم میشد (lost update) —
|
||||
مثلاً یک started_at یا یک تغییرِ status میتونست توسط نویسش دیگری
|
||||
که دیرتر commit میشه، بازنویسی/نادیده گرفته بشه.
|
||||
|
||||
حالا دقیقاً مثل الگوی grant_free_subscription در subscription/services.py:
|
||||
اول get_or_create برای اطمینان از وجودِ ردیف (idempotent، خودش با
|
||||
savepoint در برابر race روی خودِ create محافظتشده)، بعد همون ردیف با
|
||||
select_for_update دوباره خونده و قفل میشه تا خواندن/تغییر/ذخیرهی
|
||||
فیلدهاش توسط caller، در برابر concurrent update ایمن باشه. caller
|
||||
باید این تابع رو همیشه داخل transaction.atomic صدا بزنه (هر دو نقطهی
|
||||
فراخوانی در همین فایل همینطورن).
|
||||
"""
|
||||
NodeProgress.objects.get_or_create(user_id=user_id, node=node)
|
||||
progress = NodeProgress.objects.select_for_update().get(user_id=user_id, node=node)
|
||||
return progress, False
|
||||
|
||||
|
||||
def _emit_if_changed(progress, old_status, event, user_id):
|
||||
@@ -32,18 +54,19 @@ def touch_node_progress_on_answer(user_id, question):
|
||||
به in_progress برمیگرده.
|
||||
"""
|
||||
node = question.lesson.node
|
||||
progress, _created = _get_or_create_progress(user_id, node)
|
||||
old_status = progress.status
|
||||
now = timezone.now()
|
||||
with transaction.atomic():
|
||||
progress, _created = _get_or_create_progress(user_id, node)
|
||||
old_status = progress.status
|
||||
now = timezone.now()
|
||||
|
||||
if progress.status in ('not_started', 'abandoned'):
|
||||
progress.status = 'in_progress'
|
||||
if progress.started_at is None:
|
||||
progress.started_at = now
|
||||
progress.last_activity_at = now
|
||||
progress.save(update_fields=['status', 'started_at', 'last_activity_at'])
|
||||
if progress.status in ('not_started', 'abandoned'):
|
||||
progress.status = 'in_progress'
|
||||
if progress.started_at is None:
|
||||
progress.started_at = now
|
||||
progress.last_activity_at = now
|
||||
progress.save(update_fields=['status', 'started_at', 'last_activity_at'])
|
||||
|
||||
_emit_if_changed(progress, old_status, event='answer_activity', user_id=user_id)
|
||||
_emit_if_changed(progress, old_status, event='answer_activity', user_id=user_id)
|
||||
return progress
|
||||
|
||||
|
||||
@@ -54,31 +77,32 @@ def update_node_progress_on_lesson_completion(user_id, lesson):
|
||||
میره روی completed، وگرنه (یا اگه از abandoned برگشته) in_progress.
|
||||
"""
|
||||
node = lesson.node
|
||||
progress, _created = _get_or_create_progress(user_id, node)
|
||||
old_status = progress.status
|
||||
now = timezone.now()
|
||||
with transaction.atomic():
|
||||
progress, _created = _get_or_create_progress(user_id, node)
|
||||
old_status = progress.status
|
||||
now = timezone.now()
|
||||
|
||||
lesson_ids = list(node.lessons.values_list('id', flat=True))
|
||||
completed_count = UserLessonCompletion.objects.filter(
|
||||
user_id=user_id, lesson_id__in=lesson_ids
|
||||
).values('lesson_id').distinct().count()
|
||||
lesson_ids = list(node.lessons.values_list('id', flat=True))
|
||||
completed_count = UserLessonCompletion.objects.filter(
|
||||
user_id=user_id, lesson_id__in=lesson_ids
|
||||
).values('lesson_id').distinct().count()
|
||||
|
||||
progress.lessons_completed_count = completed_count
|
||||
if progress.started_at is None:
|
||||
progress.started_at = now
|
||||
progress.last_activity_at = now
|
||||
progress.lessons_completed_count = completed_count
|
||||
if progress.started_at is None:
|
||||
progress.started_at = now
|
||||
progress.last_activity_at = now
|
||||
|
||||
if lesson_ids and completed_count >= len(lesson_ids):
|
||||
progress.status = 'completed'
|
||||
progress.completed_at = now
|
||||
else:
|
||||
progress.status = 'in_progress'
|
||||
if lesson_ids and completed_count >= len(lesson_ids):
|
||||
progress.status = 'completed'
|
||||
progress.completed_at = now
|
||||
else:
|
||||
progress.status = 'in_progress'
|
||||
|
||||
progress.save(update_fields=[
|
||||
'lessons_completed_count', 'status', 'started_at', 'completed_at', 'last_activity_at',
|
||||
])
|
||||
progress.save(update_fields=[
|
||||
'lessons_completed_count', 'status', 'started_at', 'completed_at', 'last_activity_at',
|
||||
])
|
||||
|
||||
_emit_if_changed(progress, old_status, event='lesson_completed', user_id=user_id)
|
||||
_emit_if_changed(progress, old_status, event='lesson_completed', user_id=user_id)
|
||||
return progress
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user