My learning path
The Learning app does not just list courses. It leads a signed-in learner into a complete
learning path: a few questions about the project they want to build, then the courses, then a
schedule. The route has eight steps, LearningSchedule.TotalSteps:
| Steps | Stage (LearningSchedule.Stage) |
Where it happens |
|---|---|---|
| 1–6 | Questions: the LearningRoadmap questions (project, outcome, experience, confidence, pace, language) |
the learner's plan, {viewer}/LearningRoadmap/Plan |
| 7 | PickCourses: the questions are done but the route holds no course |
the plan page lists every course on the instance with Add to my path |
| 8 | Schedule: courses are planned, and some lack a start or finish day |
each stop on the plan page carries its date inputs |
| ✓ | Complete: every planned course is dated or kept for later |
the front door names what is due next |
NoPlan is the state before step 1.
The front door (Edu/Courses)
The hero card (MyLearning.Project) always shows one button that resumes exactly where the
learner stands, the step ("Step 7 of 8") and the platform's progress control:
- No plan: Build my learning path →, which opens the LearningRoadmap app (Get/Start runs in the Store, which creates the plan).
- Questions open: Continue my questions →, at the open question.
- No matching course: " — no course matched it yet. Pick yours from the full list." and Pick my courses →. The questions never end in a dead end.
- Partly scheduled: "2 courses not yet scheduled." and Schedule them →.
- Complete: the next due item, e.g. "Next: Data Modelling · Cubes by 12 March 2027", or "Overdue: …" when a day has passed, plus Open my roadmap →.
A saved legacy plan (from before the guided roadmap) just opens. The roadmap page itself offers to rebuild it.
Every course card carries the path action (MyLearning.PathActions):
- Add to my path + for a course not on the route. It opens the plan with
?add={course}, where a confirmation (RoadmapAdd) adds it with one click, says "✓ … is on your path" once it is there, or says plainly that the course is not installed on this instance and links back to the catalog. - Schedule → for a course already on the route.
- Add to my path + also appears when there is no plan yet; it starts the app.
- Nothing is shown for an anonymous visitor, or where neither the app nor a plan exists.
- The action appears only on cards the roadmap can actually add: Education plugin roots
(
LearningJourney.IsCourse), the same set its add confirmation and pick list resolve. A legacyEdu/Courseentry gets no action rather than an Add that ends in "not available". - While the questions are open (also after Refine my roadmap, which resets the step but keeps
RoadmapCreated), the add confirmation waits. The course stays named in?add=…, and the same panel becomes the Add offer once the last question is answered. The open questionnaire also wins on the front door (LearningSchedule.QuestionsOpen, the page's own rule).
The plan page
The roadmap page's questionnaire progress now counts against the whole route ("Step 2 of 8").
After the questions, a guide panel under the hero shows the step, the progress and the one line
of what to do next (GuideStep / GuideLine). Two computed slots join the stops:
RoadmapPicklists the catalog's courses not on the route, smallest first (S, M, L), each with Add to my path + and Open course →. With an empty route it is titled Pick from all courses and is step 7.- Each stop's schedule (
ScheduleBlock): Start on and Finish by, and one Done by per module. A stop planned before modules were stored offers Plan it module by module, which fills them from the course's ownlearning.milestones.
They are computed slots for the same reason the stops already were: each button or input addresses its own course key, which a bound row cannot carry.
The data — the learner's own typed node
Everything is stored on the learner's own plan node (Edu/LearningJourney,
LearningJourneyContent). Nothing is copied into a layout-area replica:
LearningCourse.TargetStart,TargetFinish(DateTime?);LearningCourse.Milestones:m00,m01, … →LearningMilestone { Order, Title, Path, Due }, filled from the course'slearning.milestoneswhen the course is recommended or added.
Every date input is a DateTimeControl bound directly to the plan node at
courses/{key}/targetStart, …/targetFinish and …/milestones/{m}/due, the same way the stop's
notes already were. There is no save button and no /data copy.
🚨 Dates are calendar facts. A target is the day the learner picked. It is shown as that day
(LearningSchedule.DateText), compared as a day, and never converted to a time zone; a test pins
that 23:30 UTC on the 12th still reads "12 March". "Today" for the overdue check is UTC's date: a
target is a day, and a boundary a few hours off for one viewer only changes whether that day already
counts as overdue.
A course the learner adds is marked Picked, and a rebuilt recommendation keeps picked courses
on the route; only recommended courses outside the new route move to an earlier plan. Completion
for the next due item reads the plan's own courses as well as the catalog's, so a finished course
the catalog no longer lists is never advertised as due.
Adding never duplicates (LearningJourney.AddFromCatalog). A course already on the route is left
exactly as it is, and an entry kept from an earlier plan comes back with its own dates. Rebuilding
the route after new answers keeps the learner's schedule: the course dates, and each module's date
matched by its page wherever it now sits.
LearningCourse has value equality including the module map. A record compares an
ImmutableDictionary by reference, which made two identical recommendations read as different.
Tests
Edu/LearningJourney → LearningScheduleTests (every route state, next due item, calendar dates,
adding, rebuild, the guide, the add confirmation) and Edu/CourseCatalog →
MyLearningTests.Hero_LeadsThroughEveryState (no plan, questions, no match, partial, complete,
German) and Cards_OfferAddOrSchedule.