Files
MathApp/content/services.py

95 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
منطق تغییر status روی NodeProgress. دو نقطه‌ی ورودی، هر دو از
content/receivers.py صدا زده می‌شن (نه مستقیم از views.py) تا اگه جای
دیگه‌ای هم (مثلاً یک import/bulk-completion از ai_tutor) رکورد UserAnswer
یا UserLessonCompletion ساخته بشه، وضعیت Node هم خودکار به‌روز بمونه —
دقیقاً همون الگوی signal-based که برای discount/subscription استفاده شده.
"""
from django.db import transaction
from django.utils import timezone
from statuslog.signals import status_changed
from .models import NodeProgress, UserLessonCompletion
def _get_or_create_progress(user_id, node):
"""get_or_create ایجاد می‌کند، سپس ردیف با select_for_update دوباره خوانده و قفل می‌شود
تا خواندن/تغییر/ذخیره در برابر concurrent update ایمن باشد. باید داخل 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):
if progress.status != old_status:
status_changed.send(
sender='content.NodeProgress', object_id=progress.id,
old_status=old_status, new_status=progress.status,
event=event, user_id=user_id,
)
def touch_node_progress_on_answer(user_id, question):
"""
وقتی کاربر به یک سوال پاسخ می‌ده (چه درست چه غلط)، یعنی روی Node
مربوطه فعالیت داشته. اگه Node هنوز شروع نشده یا رها شده بوده،
به in_progress برمی‌گرده.
"""
node = question.lesson.node
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'])
_emit_if_changed(progress, old_status, event='answer_activity', user_id=user_id)
return progress
def update_node_progress_on_lesson_completion(user_id, lesson):
"""
وقتی یک Lesson تکمیل می‌شه، تعداد Lessonهای تکمیل‌شده‌ی Node رو
به‌روز می‌کنه؛ اگه همه‌ی Lessonهای Node تکمیل شده باشن، status
میره روی completed، وگرنه (یا اگه از abandoned برگشته) in_progress.
"""
node = lesson.node
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()
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'
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)
return progress
def node_progress_summary(node):
"""برای سریالایزر: تعداد کل Lessonهای یک Node (لازم برای lessonsTotal)."""
return node.lessons.count()