Skip to content

Store denormalized review statistics (#982) - #1266

Open
IshanA2007 wants to merge 2 commits into
devfrom
982-store-course-statistics
Open

Store denormalized review statistics (#982)#1266
IshanA2007 wants to merge 2 commits into
devfrom
982-store-course-statistics

Conversation

@IshanA2007

Copy link
Copy Markdown
Collaborator

Closes #982

⚠️ Draft — substantial refactor; requires a one-time backfill after migrating

The performance win only lands after recompute_review_stats runs (see Deploy steps). Until then everything falls back to live computation (correct, just not faster).

Problem

Course/instructor review averages (rating, difficulty, enjoyability, recommendability, hours, reading/writing/group/homework) were aggregated from all Review rows on every requestCourse.average_*, Course.with_stats, Instructor.average_*_for_course, and the course-instructor view.

Fix

Denormalized storage, mirroring the existing CourseGrade / CourseInstructorGrade split:

  • New CourseStats (OneToOne → Course, course-level rollup) and CourseInstructorStats (per course+instructor), sharing an abstract base with review_count + a stored average for every aggregated Review field. Composite rating is derived from its 3 stored components (single source of truth, matching the historic formula). Only non-hidden reviews are counted.
  • post_save / post_delete / pre_save signals on Review recompute the affected (course, instructor) pair + course rollup on every create / edit / delete / re-point / hide; rows with zero remaining reviews are deleted.
  • python manage.py recompute_review_stats backfills / rebuilds all rows from existing reviews.
  • All read paths rewritten to use stored stats with a safe fallback to live aggregation when a row is missing — so nothing breaks pre-backfill. Public method names/return types are unchanged; templates untouched.

Migration: 0029_coursestats_courseinstructorstats_and_more.py.

Deploy steps

  1. Apply migration 0029.
  2. Run python manage.py recompute_review_stats once.

Verification (run against a disposable Postgres)

  • makemigrations --check --dry-run — no changes detected (models match migration 0029).
  • manage.py check clean; ruff check / ruff format --check clean.
  • Full suite manage.py test tcf_website — 267 OK, incl. 10 new tests (signal maintenance on create/edit/delete/move, hidden-review exclusion, stored-vs-live numeric equivalence, backfill command).

Risks for review

  • with_stats now LEFT JOINs the stats OneToOne with Coalesce to correlated subqueries (fallback dormant post-backfill) — worth an EXPLAIN on prod-sized data.
  • Bulk imports that bypass ORM save()/signals (e.g. load_review_drive) won't update stats incrementally — re-run the backfill afterward.
  • No-arg Instructor.average_rating() / average_difficulty() (all-courses; no current consumers) intentionally left as live computation.
  • Confirm the signals compose with the existing Cachalot/Redis cache as desired.

🤖 Implemented with Claude Code.

Course review averages (rating components, difficulty, hours breakdowns,
recommendability, enjoyability, instructor rating) were re-aggregated from
every Review on each request. Store these aggregates and maintain them
incrementally instead.

- Add CourseStats (per-course rollup) and CourseInstructorStats (per
  course+instructor) models, each holding review_count plus the average of
  every Review field the read paths aggregate. Composite "rating" is derived
  from the three stored rating components (single source of truth).
- Maintain stats via post_save/post_delete signals on Review, recomputing
  from scratch for the affected (course, instructor) pair and course rollup.
  A pre_save hook also refreshes the old target when a review is re-pointed
  or hidden. Empty aggregates delete the row so nothing stale lingers.
- Add recompute_review_stats management command to backfill from existing
  reviews.
- Update read paths (Course.average_rating/difficulty, with_stats,
  Instructor.average_*_for_course, get_instructors_and_data, and the
  course_instructor view chart data) to read stored values, each with a SAFE
  FALLBACK to live aggregation when a stats row is missing (pre-backfill).
  Public method names/return types are unchanged so templates keep working.
- GPA/grade averages (CourseGrade/CourseInstructorGrade via load_grades) are
  intentionally untouched — out of scope for #982.

Adds tests for signal maintenance (create/edit/delete/move), hidden-review
exclusion, stored-vs-live equivalence, and the backfill command.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@IshanA2007, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a104b256-b8c6-46c2-907a-ace2cca3217d

📥 Commits

Reviewing files that changed from the base of the PR and between ed955fb and 23c0c0a.

📒 Files selected for processing (9)
  • tcf_website/apps.py
  • tcf_website/management/commands/recompute_review_stats.py
  • tcf_website/migrations/0029_coursestats_courseinstructorstats_and_more.py
  • tcf_website/models/__init__.py
  • tcf_website/models/models.py
  • tcf_website/models/stats.py
  • tcf_website/signals.py
  • tcf_website/tests/test_review_stats.py
  • tcf_website/views/courses/course_instructor.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 982-store-course-statistics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@IshanA2007
IshanA2007 marked this pull request as ready for review July 8, 2026 18:53
@IshanA2007 IshanA2007 self-assigned this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Course Statistics

1 participant