give the leaderboard and notifications a real producer
Three gaps found while seeding test data for the frontend, all of the same kind: a contract that looks complete from the outside and is empty underneath. Nothing in the codebase ever wrote a LeaderboardEntry. Not a signal, not a service — only the admin form and seed_test_data. So /api/leaderboard returned `entries: []` forever on any real server, and any row put there was a frozen number that never tracked activity. rebuild_leaderboard now derives both periods from real XP: all_time from UserStats.xp, weekly from a sum over content.UserLessonCompletion in a window. It deliberately does not count UserAnswer.xp_earned — that field is written but never added to UserStats, so summing both would report roughly double. deploy.sh runs it every release; a cron is the real answer once traffic is real, and the script says so. 'lesson_completed' was declared in Notification.TYPE_CHOICES but nothing called notify() with it, so finishing a lesson produced no notification at all. LessonCompleteView now emits one, on first completion only (the same condition that grants xp/coins), inside the completion transaction so a lost notification cannot leave a silent half-done state. The notification list serializer returned only id/type/title/isRead. body and created_at were on the model from the start but never exposed, so the frontend had neither the text of a notification nor its time. Both added; the change is purely additive, so the existing client keeps working. 472 tests green, up from 458. makemigrations --check, check and spectacular are all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,25 @@ class LessonCompleteView(APIView):
|
||||
).values('lesson_id').distinct().count()
|
||||
stats.progress_percent = round(min(completed_lessons / total_lessons, 1.0) * 100, 2)
|
||||
stats.save(update_fields=['xp', 'coins', 'progress_percent'])
|
||||
|
||||
# اعلانِ درسِ تکمیلشده. تا پیش از این، نوعِ 'lesson_completed'
|
||||
# در Notification.TYPE_CHOICES تعریف شده بود ولی هیچجای کد
|
||||
# آن را نمیساخت — یعنی کلِ مسیرِ کوییز هیچ اعلانی تولید
|
||||
# نمیکرد. فقط بارِ اول (همان شرطی که xp/coins میدهد)؛
|
||||
# تکرارِ درس اعلانِ تازه نمیسازد.
|
||||
#
|
||||
# عمداً داخلِ همین transaction است: اگر نوشتنِ اعلان شکست
|
||||
# بخورد، تکمیلِ درس هم برنمیگردد — همالگو با
|
||||
# campaigns.services که اعلان و وضعیت را یک واحد میداند.
|
||||
if is_first_completion:
|
||||
from notifications.services import notify
|
||||
notify(
|
||||
user_id=user_id,
|
||||
type='lesson_completed',
|
||||
title='درس را کامل کردی',
|
||||
body=f'«{lesson.title}» را با {stars} ستاره تمام کردی و {xp_earned} امتیاز گرفتی.',
|
||||
related_id=lesson.id,
|
||||
)
|
||||
except IntegrityError:
|
||||
# قید یکتاییِ سطح DB جلوی درخواستهای همزمان با همان کلید را میگیرد؛
|
||||
# نتیجهی completion رقیب idempotent برگردانده میشود.
|
||||
|
||||
Reference in New Issue
Block a user