fix HomeView N+1 and cache the curriculum tree

GET /api/home is the most-hit authenticated endpoint (every app open) and
was running one COUNT per node. Now it is a flat number of queries
regardless of grade size, and the shared curriculum skeleton is served
from cache; per-user progress/stats stay live.

content/views.py
  - _node_status(): takes a precomputed set of completed lesson_ids and a
    node's lesson_ids instead of firing its own COUNT query.
  - HomeView.get(): one query for all completed lesson_ids of the user in
    the grade, status computed in Python; grade tree (units -> nodes ->
    lesson_ids, no per-user data) read from cache
    (content:tree:grade:{id}:v{N}); missing grade -> 404 (was an
    unhandled 500); grade with no units -> empty units, not 404.
  - GradesListView: served from cache (content:grades:v{N}).
  - curriculum_version() / bump_curriculum_version(): one shared version
    key; bumping it invalidates every content cache key at once.
  - both GET views get throttle_scope='read' + GuestAwareContentThrottle.
content/receivers.py
  - post_save/post_delete on Grade/Unit/Node/Lesson -> bump_curriculum_version().
content/tests.py
  - HomeViewScalabilityTests: query count flat for 2 vs 20 nodes; 2nd call
    hits cache (no content_unit/node SQL); a new Node invalidates the tree;
    missing grade 404; empty grade 200.
  - GradesListCacheTests: list cached, invalidated on new Grade.
  - ReadThrottleTests: /api/home returns 429 past the limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 08:45:18 +03:30
parent eeb6bd6138
commit 35218bf6a1
3 changed files with 231 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
from django.db.models.signals import post_save
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from .models import UserAnswer, UserLessonCompletion
from .models import Grade, Lesson, Node, Unit, UserAnswer, UserLessonCompletion
from .services import touch_node_progress_on_answer, update_node_progress_on_lesson_completion
@@ -17,3 +17,18 @@ def _on_lesson_completion_created(sender, instance, created, **kwargs):
if not created:
return
update_node_progress_on_lesson_completion(instance.user_id, instance.lesson)
# با هر تغییرِ برنامه‌ی درسی (از ادمین یا seed)، نسخه‌ی کشِ محتوا یک واحد جلو
# می‌رود و همه‌ی کلیدهای content:grades / content:tree را یک‌جا باطل می‌کند.
@receiver(post_save, sender=Grade, dispatch_uid='content_bump_curriculum_grade_save')
@receiver(post_delete, sender=Grade, dispatch_uid='content_bump_curriculum_grade_delete')
@receiver(post_save, sender=Unit, dispatch_uid='content_bump_curriculum_unit_save')
@receiver(post_delete, sender=Unit, dispatch_uid='content_bump_curriculum_unit_delete')
@receiver(post_save, sender=Node, dispatch_uid='content_bump_curriculum_node_save')
@receiver(post_delete, sender=Node, dispatch_uid='content_bump_curriculum_node_delete')
@receiver(post_save, sender=Lesson, dispatch_uid='content_bump_curriculum_lesson_save')
@receiver(post_delete, sender=Lesson, dispatch_uid='content_bump_curriculum_lesson_delete')
def _bump_curriculum_cache_version(sender, **kwargs):
from .views import bump_curriculum_version
bump_curriculum_version()