Skip to content

fix(reviews): put reviews where they can be read and written - #140

Merged
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/136-reviews-reachable
Sep 1, 2026
Merged

fix(reviews): put reviews where they can be read and written#140
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/136-reviews-reachable

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Closes #136.

The defect

The reviews feature added in #18 is complete on the server and was all but
unreachable in the browser.

GET /api/reviews/:courseId is deliberately public — no authMiddleware on
that line — and createReview gates on enrolment and nothing else:

const enrollment = await ensureEnrollment(userId, courseId);
if (!enrollment) {
  return res.status(403).send({
    success: false,
    message: "Only enrolled students can review this course.",
  });
}

<CourseReviews>, the component that consumes all of it, was rendered in
exactly one place in the entire frontend — inside <Modal.Body> of the
certificate modal in CourseContent.jsx. That modal opens from a button
that only renders once every section is complete.

Nobody could read a review. Every catalogue card renders a
<CourseRatingBadge> showing a star average and a count. It was an inert
<div>: not a link, not a button, with no expanded form and no course detail
page to navigate to. A prospective student saw "4.6 (23)" and had no route to
any of the 23, on the one screen where reviews exist to inform a decision. The
public listReviews endpoint had no caller in the frontend at all.

Most enrolled students could not write one. The server's rule is enrolment;
the UI's rule was 100% completion. A student nine sections into a ten-section
course was authorised by the API and had no button. Reviews were therefore
collected only from the people who finish courses, which biases every average
on the catalogue upward — invisibly, because the count looks like it covers all
enrolments.

What is here

The rule, stated as a rule. frontend/src/lib/reviewAccess.js holds the
four states a viewer can be in, replacing three nested ternaries in the
component.

The important property is a negative one: describeReviewAccess takes no
progress argument
. Completion is not part of the server's rule and must not
become part of this one — that divergence is the defect, and a function with no
input for it cannot drift back to one.

One case worth flagging for review: /mine reports canReview as "may create
a new one", which goes false the moment a review exists. Reading it as "may
not submit" would make a review uneditable as soon as it was written, so an
existing review implies the enrolment that allowed it and keeps its edit path.

Reviews on the course player. <CourseReviews> moves out of the
certificate modal and onto the page, below the sections and the video, where
any enrolled student reaches it at any progress. The certificate modal keeps
its congratulation and its download — it is a reasonable prompt, just not the
only door.

Reviews from the catalogue. <CourseRatingBadge> takes an optional
onOpen. With it the badge renders as a <button> that opens the course's
reviews; without it it is the same inert div as before, so the single-course
usages are unaffected. AllCourses renders <CourseReviews> in a scrollable
modal beside the payment one.

The accessible name is the whole sentence — "Rated 4.6 out of 5 from 23 reviews
for Intro to Testing. Open reviews" — because the stars, the average and the
count render as three separate nodes, which is not a sentence, and a control
should say what activating it does.

Nothing is fetched until the badge is activated, so the catalogue's critical
path is unchanged: the summaries are still batched by useRatingSummaries in
the one request that already existed. This does not reintroduce the N+1 that
#86 removed.

Reading works signed out. CourseReviews already had the message for that case
— it had simply never been rendered anywhere a signed-out visitor could reach.

Tests

frontend/src/lib/reviewAccess.test.js — 12 tests. The regression is "an
enrolled student may write, at any progress". Alongside it: a signed-out
visitor may read but not write; a signed-in non-enrolled student is told to
enrol; an existing review stays editable once canReview goes false; a review
held by a signed-out viewer is still not writable; no viewer at all does not
throw; every state names itself and exactly the blocked ones carry a message;
and the badge label reads as a sentence, singularises one review, says "no
reviews yet" rather than "0 reviews", and produces no NaN from a missing
summary.

docs/issue-136-reviews-reachable.md has the write-up.

Checklist

  • cd frontend && npm test — 226 pass (214 on main, 12 added)
  • cd frontend && npm run build
  • cd backend && npm test — 505 pass, untouched by this change
  • npm run lint — does not pass on main (69 problems) and does not here.
    AllCourses.jsx reports the same 5 before and after, CourseContent.jsx
    the same 3, CourseReviews.jsx the same 2 — all pre-existing
    no-unused-vars and react/prop-types. lib/reviewAccess.js and
    CourseRatingBadge.jsx lint clean.

Notes

No API change: every endpoint used here already existed and is untouched. There
is still no course detail page — the catalogue modal is the reading surface,
which suits a card grid but is not a shareable URL for a course and its
reviews; a /course/:courseId route is a larger change and separate work. The
upward bias in the averages collected while this was broken is in the data
already, and nothing here rewrites it.

@udaycodespace
udaycodespace self-requested a review August 30, 2026 06:28
@udaycodespace udaycodespace added ECSoC26 Required label for a PR to be eligible for Sentinel scoring in review PR is up and waiting on maintainer review enhancement New feature or improvement request labels Aug 30, 2026
@udaycodespace

Copy link
Copy Markdown
Owner

@MOHITKOURAV01 Please resolve the merge conflicts in CourseReviews.jsx and CourseContent.jsx.

The implementation looks good and the checks are passing. Once the conflicts are cleared and the branch is up to date with main, this PR should be good to merge.

@udaycodespace udaycodespace added redo Reviewed — needs changes before it can be merged and removed in review PR is up and waiting on maintainer review labels Aug 31, 2026
The reviews feature is complete on the server and was all but unreachable in
the browser. <CourseReviews> was rendered in exactly one place in the entire
frontend — inside the certificate modal on the course player, which only opens
once every section is complete.

So nobody could read a review. Every catalogue card renders a rating badge
showing a star average and a count, and it was an inert div: no link, no
button, no expanded form, and no course detail page to navigate to. A
prospective student saw "4.6 (23)" with no route to any of the 23. The public
GET /api/reviews/:courseId had no caller in the frontend at all.

And most enrolled students could not write one. The server asks for enrolment
and nothing else; the UI asked for 100% completion. A student nine sections
into ten was authorised by the API and had no button, so reviews were collected
only from the people who finish courses — which biases every average on the
catalogue upward, invisibly, because the count looks like it covers all
enrolments.

That gate was never a predicate to fix — reviewEligibility.js already states
the server's rule, and asks for enrolment and non-authorship, not progress. The
completion check was the mount point. So CourseReviews moves out of the
certificate modal and onto the player page, below the sections and the video,
where an enrolled student reaches it at any progress. The modal keeps its
congratulation and its download.

CourseRatingBadge takes an optional onOpen and renders as a button when it has
one, opening the course's reviews from the catalogue; without it, it is the
same div as before, so the single-course usages are unaffected. Its accessible
name is the whole sentence, because three separate nodes are not one and a
control should say what it does. Nothing is fetched until the badge is
activated, so the catalogue's critical path is unchanged.

Reading works signed out. CourseReviews already had the message for that case;
it had never been rendered anywhere a signed-out visitor could reach it.
@MOHITKOURAV01
MOHITKOURAV01 force-pushed the fix/136-reviews-reachable branch from 708477f to 491993b Compare August 31, 2026 10:15
@MOHITKOURAV01

Copy link
Copy Markdown
Contributor Author

@udaycodespace rebased on latest main — conflicts resolved, but this one changed shape, so it is worth a second look.

#122 landed reviewEligibility.js, which covers the same ground as the reviewAccess.js I added and also handles the own-course denial. I dropped mine in favour of yours.

The hasReview case I had is gone too. It assumed /mine reports canReview: false once a review exists, but the endpoint now computes !isAuthor && Boolean(enrollment), so that never happens — keeping it would have been a rule contradicting the server.

What is left is placement only: <CourseReviews> moves out of the certificate modal onto the player page, and the catalogue rating badge becomes a button that opens a course’s reviews. reviewAccess.js is now just the badge label helper. Commit message and the docs write-up updated to match; tests went 12 → 5, since who may write a review is covered by reviewEligibility.test.js.

Frontend 330 pass.

@udaycodespace

Copy link
Copy Markdown
Owner

@udaycodespace rebased on latest main — conflicts resolved, but this one changed shape, so it is worth a second look.

#122 landed reviewEligibility.js, which covers the same ground as the reviewAccess.js I added and also handles the own-course denial. I dropped mine in favour of yours.

The hasReview case I had is gone too. It assumed /mine reports canReview: false once a review exists, but the endpoint now computes !isAuthor && Boolean(enrollment), so that never happens — keeping it would have been a rule contradicting the server.

What is left is placement only: <CourseReviews> moves out of the certificate modal onto the player page, and the catalogue rating badge becomes a button that opens a course’s reviews. reviewAccess.js is now just the badge label helper. Commit message and the docs write-up updated to match; tests went 12 → 5, since who may write a review is covered by reviewEligibility.test.js.

Frontend 330 pass.

Really good PR, @MOHITKOURAV01 ! Unfortunately, it won’t get scored since the program deadline has already passed. Still, really appreciate the effort you put in. ❤️

All the best for your upcoming OSS programs!

@udaycodespace udaycodespace removed the redo Reviewed — needs changes before it can be merged label Sep 1, 2026
@udaycodespace
udaycodespace merged commit 7b34462 into udaycodespace:main Sep 1, 2026
1 check passed
@ecsoc-sentinel ecsoc-sentinel Bot added the ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points ECSoC26 Required label for a PR to be eligible for Sentinel scoring enhancement New feature or improvement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Course reviews are unreachable — the public API has no consumer and the only review UI is behind the 100%-completion certificate modal

2 participants