fix(courses): let every section be completed and date the certificate properly - #98
Open
MOHITKOURAV01 wants to merge 4 commits into
Open
fix(courses): let every section be completed and date the certificate properly#98MOHITKOURAV01 wants to merge 4 commits into
MOHITKOURAV01 wants to merge 4 commits into
Conversation
… properly
The course player hid the Completed button inside `{section.S_content && ...}`,
so a section with no video had no control to complete it. course_Length counts
every section, so one such section made the total unreachable and the
certificate never appeared.
The certificate gate was `completedModule.length === courseContent.length`,
computed in the browser over the enrolment's raw progress array. That array can
hold the same sectionId twice — rows written before udaycodespace#39 had no uniqueness
guard, which is why countCompletedSections exists — and ids for sections the
course no longer has. Both inflate the length, so the comparison could unlock a
certificate early as well as withhold one that was earned.
And the certificate was dated `certficateData.updatedAt`, Mongoose's last-write
timestamp, which moves on every progress save. `certificateDate` has been on
enrolledCourseModel since it was written and nothing ever set it.
utils/courseProgress.js holds the rule now: countCompletedSections and
buildProgressSummary move there out of enrolledCoursesController, which imports
them back, so the player and My Courses cannot disagree about one enrolment.
describeSections keeps hasVideo and completed independent, and resolves
completion against both an index and a section _id.
courseContentController replaces sendCourseContentController: the enrolment is
resolved from req.user rather than req.body.userId, an unenrolled caller gets
403 instead of 404 "User not found", a malformed id is a 400 rather than a
CastError, and the whole enrolment document is no longer shipped as
certficateData. completemodule returns the recomputed summary and stamps
certificateDate once, under a filter guarded on the field still being unset.
The player renders a progress bar, offers Mark complete on every section,
gates the certificate on the server's isComplete, omits the date line when
there is none, handles a failed load, and uses the Toast from udaycodespace#36 instead of
alert().
Closes udaycodespace#93
App.css was gaining an appended block per feature branch, which conflicts on every pair of them and says nothing about where a rule belongs. The player's rules live next to the player instead, imported by the component that needs them.
The aggregator's module.exports block is a single region that several branches touch at once, and renaming a key inside it collides with any neighbouring line. Binding the old name to the new controller next to the require says the same thing and leaves the shared block alone.
Contributor
Author
Overlap with #81, worth knowing aboutBoth merge cleanly into
They pull in the same direction and the resolution is mechanical, but it is a real merge rather than a union of unrelated lines. The Whichever you take second, tell me and I will push the resolution. |
38 tasks
udaycodespace
self-requested a review
August 24, 2026 15:50
38 tasks
Both conflicts are the same shape: an import block where this branch and main each added a different neighbour. userRoutes and userControllers each gained getCourseContentController here and the resend-OTP wiring on main (udaycodespace#73). Nothing competes — both sides are kept. No behavioural change from the merge. Backend 433 pass, frontend 184, build clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The course player hid the Completed button inside
{section.S_content && ...}, decided completion in the browser from an array that is known to contain duplicates and stale ids, and dated the certificate from the enrolment's last-write timestamp. This makes the server the authority on all three.Related Issue
Closes #93
What changed?
backend/utils/courseProgress.js(new) — the progress rule in one place.countCompletedSectionsandbuildProgressSummarymoved here out ofenrolledCoursesController, which imports and re-exports them, so the player and My Courses cannot report different numbers for the same enrolment. AddscompletedSectionIds,isEnrollmentCompleteanddescribeSections.describeSectionsreadssectionsthroughnormalizeSections, so an object-shaped field is described in order, and resolvescompletedagainst both addressing schemes — an index and a section_id— becausenormalizeSectionIdaccepts either and older rows hold whichever the client sent.hasVideoandcompletedare deliberately independent fields; nothing downstream may derive the second from the first.backend/controllers/courseContentController.js(new) — replacessendCourseContentControllerin theuserControllersaggregator:req.user, notreq.body.userId— the same change [Security]: A teacher can publish a course under someone else's account — /addcourse takes userId from the upload form #83 made to/addcourse;:courseidis validated before it reaches Mongoose, so a malformed id is a 400 rather than aCastErrorsurfacing as a 500;progress,isComplete,certificateDate,courseTitleandcourseEducator;certficateData— the entire enrolment document, sent for the sake of one field — is gone;courseContentandcompleteModulekeep their original names, so anything still reading them keeps working.backend/controllers/progressController.js—completemodulereturns the recomputedprogress,isCompleteandcertificateDate, so the common case needs no follow-up request, and stamps the certificate when the last section lands:Guarded on the field still being unset, so two requests completing the last section at once cannot overwrite each other's date. The summary is projected from the array that was read plus the id
$addToSetjust wrote, so there is no second read.frontend/src/lib/courseProgress.js(new) — normalises the response. Recomputes nothing the server sent:readIsCompletetrustsisCompleteand falls back to comparing the summary, never to a bare array length.readVideoPathhandles/uploads/x,uploads/x,x,\xand absolute URLs, which the component used to do inline inside a JSXonClick.frontend/src/components/user/student/CourseContent.jsx— Mark complete is outside thehasVideoguard; a progress bar andn of m sections completeat the top; the certificate is gated on the server'sisCompleteand dated fromcertificateDate, with the date line omitted rather than renderingInvalid Date; loading and error states with a retry;alert()replaced with theToastcomponent from #36, reflecting thealreadyCompletedflag.Type
Areas touched
certificateDatewas already declared onenrolledCourseModel. This is the first code that writes it; no migration is needed, and enrolments completed before this ship keepcertificateDate: nulland simply show no date line.Testing
npm testinbackend/— 254 passing, up from 234 onmain.npm testinfrontend/— 89 passing, up from 73.npm run buildinfrontend/— clean.npm run linthas never passed onmain: it reports 69 problems there, almost all pre-existingno-unused-varson React imports and missingreact/prop-types.npx eslintis clean on every file this PR adds or rewrites.Test steps
db.courses.updateOne({C_title: "Introduction to HTML and CSS"}, {$unset: {"sections.1.S_content": ""}}).mainit renders no controls at all and the certificate is unreachable from here.db.enrolledcourses.findOne(...)→certificateDateis set. Complete a section again: the toast says it was already complete and the date does not move.403 "You are not enrolled in this course", wheremainsays404 "User not found".Screenshots
Edge cases checked
Other edge case details
isEnrollmentCompleterequirestotal > 0, so an enrolment created against an empty course does not read as finished on first render.total, so they cannot unlock a certificate.updateOnematches nothing and the first stamp stands.sectionsstored as an object map is described in order rather than dropped._idbefore the index scheme is still shown as complete.Checklist
CONTRIBUTING.mdNotes
docs/issue-93-course-completion.mdhas the full write-up, including why the oldcompletedModule.length === courseContent.lengthwas wrong in both directions.The clock in
progressControlleris injectable (setClock) purely so a test can assert the stamped date without racing it; production behaviour is unchanged.Update: the stylesheet moved out of
App.cssI originally appended this feature's rules to the end of
frontend/src/App.css. Four of the branches I have in flight were each doing that, which conflicts on every pair of them — nine conflicting pairs, none of them about anything real.They now live in
frontend/src/styles/course-player.css, imported by the component that needs them. The shared classes this feature reuses (catalog-search,catalog-filter, the design tokens) stay inApp.css; only what is new to #93 moved.That was the right call independently of git — a rule for one screen belongs next to that screen, not at the bottom of a growing global sheet — but the concrete effect is that all five of my open feature PRs now merge into
mainand into each other with no conflicts at all. I verified it by merging all five together and running both suites on the combined tree: 317 backend tests and 127 frontend tests pass, and the frontend build is clean.