Skip to content

Rebuild project like flow#108

Merged
gregagi merged 5 commits into
masterfrom
forge/rebuild-project-likes
Jun 12, 2026
Merged

Rebuild project like flow#108
gregagi merged 5 commits into
masterfrom
forge/rebuild-project-likes

Conversation

@gregagi

@gregagi gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • render project like counts and viewer state from annotated project queries instead of per-card API reads
  • add an authenticated project-like toggle endpoint that returns the updated state/count
  • simplify the Stimulus like controller to render from server data and make one POST per click

Production/backfill notes

  • no schema or data migration added
  • no backfill needed for this change; existing Like rows remain the source of truth

Verification

  • uv run --with pytest --with pytest-django --with django --with djangorestframework --with django-filter --with django-model-utils --with django-autoslug --with django-environ --with django-widget-tweaks --with django-q2 --with requests --with markdown --with cloudinary --with django-webpack-loader --with pillow --with posthog --with structlog --with django-structlog --with sentry-sdk --with structlog-sentry --with pydantic-ai-slim[openrouter] --with logfire --with django-allauth --with django-oauth-toolkit --with django-anymail --with django-storages --with boto3 --with stripe pytest
  • npm run build

@gregagi

gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces per-card AJAX like reads with server-rendered counts and state from annotated querysets, and consolidates writes into a single authenticated toggle endpoint. The Stimulus controller is simplified from ~200 lines to ~110 by reading from Stimulus value attributes instead of firing API calls on connect().

  • projects/querysets.py (new): with_like_metadata() annotates any project queryset with like_count (COUNT with distinct=True to survive joins) and user_has_liked (Exists subquery for authenticated users, Value(False) otherwise). All three project views and the home page now route through it.
  • ProjectLikeToggleAPIView: New authenticated POST endpoint at projects/<id>/like/ that returns the server-confirmed like state and updated count; the existing_like lookup is correctly guarded inside the auto-toggle fallback branch.
  • Stimulus controller: connect() pre-renders from server values (idempotent since the template already emits the correct initial state); modify() sends a single POST and reconciles UI from the response; errors surface via showError() instead of silently swallowing.

Confidence Score: 5/5

Safe to merge — the change eliminates N+1 API reads on page load, all views route through the new annotation helper, and the new toggle endpoint is tested end-to-end.

The concerns flagged in earlier review rounds (unconditional existing_like query, analytics reporting pre-request state, invisible heart icon before hydration, silent error handling) are all addressed in the current revision. The with_like_metadata helper is correct: distinct=True in the COUNT keeps results accurate when the outer queryset introduces additional joins, and the authenticated Exists subquery is gated properly. No new logic issues were found.

No files require special attention.

Important Files Changed

Filename Overview
projects/querysets.py New helper that annotates project querysets with like_count and user_has_liked; uses Count with distinct=True to stay correct across joins, and correctly falls back to Value(False) for anonymous users.
api/views.py Adds ProjectLikeToggleAPIView; existing_like query is correctly guarded inside the if like_value is None branch, resolving the unconditional-read concern from previous review rounds.
frontend/src/controllers/like_controller.js Simplified from 200 lines to ~110; reads from Stimulus values instead of on-connect API calls; trackChange now passes this.likedValue (server-confirmed) and error feedback via showError() is implemented.
templates/components/project-likes.html Pre-renders like count and heart icon class server-side from annotations; heart icon is visible before JS hydrates, resolving the earlier flash-of-invisible-icon issue.
projects/views.py All three project views now route through with_like_metadata; the conditional like__count annotation for sort-by-like is replaced by the always-present like_count annotation; control flow is cleaner.
pages/views.py Home page projects queryset is now annotated via with_like_metadata; slicing [:6] is applied after annotation (correct ordering for Django ORM).
api/tests.py Good coverage: auth guard, create, update, and implicit-toggle test cases added for the new toggle endpoint.
api/urls.py New RESTful route projects/<project_id>/like/ registered and named api_project_like_toggle.
projects/tests.py New test verifies like_count and user_has_liked annotations on the project list view queryset.
pages/tests.py New test confirms home-page projects include annotation metadata and that user_has_liked reflects the requesting user's actual like state.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Django as Django (SSR)
    participant DB as Database
    participant Stimulus as Stimulus Controller
    participant API as ProjectLikeToggleAPIView

    Browser->>Django: GET /projects/
    Django->>DB: with_like_metadata(queryset, user) annotate(like_count, user_has_liked)
    DB-->>Django: Projects + counts + viewer state
    Django-->>Browser: "HTML with data-like-*-value attrs + pre-rendered heart icon and count"

    Note over Browser,Stimulus: JS hydrates — connect() reads values, calls render() (idempotent)

    Browser->>Stimulus: User clicks heart
    Stimulus->>API: "POST /api/v1/projects/{id}/like/ {like: true/false}"
    API->>DB: update_or_create Like
    DB-->>API: like row
    API->>DB: "COUNT(like=True) for project"
    DB-->>API: like_count
    API-->>Stimulus: "{like: bool, like_count: int}"
    Stimulus->>Stimulus: Update likedValue, countValue then render()
    Stimulus->>Stimulus: trackChange(serverLiked, prevCount)
Loading

Reviews (6): Last reviewed commit: "Show like toggle errors" | Re-trigger Greptile

Comment thread api/views.py
Comment thread frontend/src/controllers/like_controller.js Outdated
Comment thread templates/components/project-likes.html Outdated
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the old per-card API read approach for project likes with server-side annotation (with_like_metadata) applied in all three project views, a new authenticated toggle endpoint (ProjectLikeToggleAPIView), and a simplified Stimulus controller that reads initial state from server-rendered values and makes a single POST per click.

  • with_like_metadata annotates querysets with like_count and user_has_liked using Count + Exists subqueries; applied consistently to ProjectListView, InactiveProjectListView, and ProjectDetailView.
  • ProjectLikeToggleAPIView handles create/update via update_or_create, returns the confirmed like state and count, and fires a PostHog event.
  • The Stimulus controller shrinks from 204 lines to 94, dropping three round-trips per card on load in favour of a single POST on click; HomeView in pages/views.py is not yet updated and will render 0 for every like count on the home page.

Confidence Score: 3/5

The list and detail views are correct, but the home page will show 0 likes for all projects because its queryset skips the new annotation step.

The three views covered by the PR work correctly and are well-tested. However, HomeView in pages/views.py fetches projects with a plain queryset that does not call with_like_metadata, while the shared project-card.html template now reads site.like_count and site.user_has_liked as server-rendered values. Every project displayed on the home page will show a like count of 0 and an unliked heart, which is a visible regression for the most prominent page on the site.

pages/views.py — HomeView's project queryset needs with_like_metadata applied before the [:6] slice

Important Files Changed

Filename Overview
pages/views.py HomeView fetches projects without with_like_metadata, causing home page cards to always display 0 likes and an unliked heart
api/views.py New ProjectLikeToggleAPIView correctly handles like/unlike with update_or_create; minor extra unconditional DB read when client always sends an explicit like value
projects/views.py with_like_metadata helper cleanly annotates querysets for all three project views; ordering-by-like refactor is simpler and correct
frontend/src/controllers/like_controller.js Greatly simplified Stimulus controller reads initial state from server-rendered values; analytics event uses pre-request prediction and errors are silently swallowed
templates/components/project-likes.html Template correctly wires Stimulus values from server-rendered annotations; CSRF token and auth branches are intact
api/tests.py Good coverage of the toggle endpoint: auth, create, update, and implicit-toggle cases all tested
projects/tests.py New test verifies like_count and user_has_liked annotations correctly count only true likes and reflect the requesting user's state
api/urls.py New toggle route added cleanly alongside existing like endpoints

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Django as Django View
    participant DB as Database
    participant Stimulus as like_controller.js
    participant API as ProjectLikeToggleAPIView

    note over Django,DB: Page load
    Django->>DB: Project.objects.filter() + with_like_metadata(user)
    DB-->>Django: Projects annotated with like_count, user_has_liked
    Django-->>Browser: HTML with data-like-count-value, data-like-liked-value
    Browser->>Stimulus: connect() → render()
    note over Stimulus: Sets heart icon class and count from server values

    note over Browser,API: User clicks Like (authenticated)
    Browser->>Stimulus: modify()
    Stimulus->>Stimulus: setPending(true), compute nextLiked
    Stimulus->>API: "POST /api/v1/projects/{id}/like/ {like: nextLiked}"
    API->>DB: "update_or_create(author, project, like=like_value)"
    API->>DB: "Count likes where like=True"
    DB-->>API: like_count
    API-->>Stimulus: "{like, like_count}"
    Stimulus->>Stimulus: update likedValue, countValue, render()
    Stimulus->>Stimulus: setPending(false)
Loading

Reviews (2): Last reviewed commit: "Address like review feedback" | Re-trigger Greptile

@gregagi

gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread api/views.py
Comment thread frontend/src/controllers/like_controller.js
Comment thread frontend/src/controllers/like_controller.js
@gregagi

gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

@gregagi

gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

@gregagi

gregagi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

@gregagi gregagi merged commit 546ff7f into master Jun 12, 2026
3 checks passed
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.

1 participant