Skip to content

Release 0.81.9 - #4060

Open
odlbot wants to merge 6 commits into
releasefrom
release-candidate
Open

odlbot wants to merge 6 commits into
releasefrom
release-candidate

Conversation

@odlbot

@odlbot odlbot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Nathan Levesque

Ahtesham Quraish

Carey P Gumaer

Anastasia Beglova

abeglova and others added 6 commits October 6, 2026 10:14
* fix(dashboard): rely on the API's contract scoping when picking a run

On a contract page the courses API already limits each course's runs to
that contract, counting runs attached through the run's contract list as
well as its b2b_contract field. getBestRun then filtered again on
b2b_contract alone, which dropped runs whose b2b_contract is blank, so
the card showed no run at all.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(dashboard): show staff enrollments in contract runs with no b2b_contract

An enrollment's b2b_contract_id mirrors its run's b2b_contract, which is
left blank on contract runs learners must not enroll in themselves. The
contract page dropped those enrollments, so a staff-enrolled learner saw
a disabled Start instead of Continue. Keep a null-contract enrollment on
a contract page when its run is one the API returned for that contract.

EnrolledCourseCard now takes the page's contractId, like
UnenrolledCourseCard, so these cards still read "Module" and never offer
a certificate upgrade.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(dashboard): keep contract enrollments off My Learning

My Learning treated any enrollment with a null b2b_contract_id as a
personal one. A contract run with a blank b2b_contract (staff-enrolled
only) produces exactly that, so it showed up as a "Course" card with a
certificate upgrade banner. Ask the enrollments API for exclude_b2b,
which filters on the run's b2b_contracts list.

The filtered list gets its own query key that extends the shared one, so
existing invalidations still refresh it, and the unenroll mutation's
optimistic removal now updates every variant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* revert(dashboard): drop the exclude_b2b query for My Learning

Since mitodl/mitxonline#4084, the enrollments API's exclude_b2b and an
enrollment's b2b_contract_id both come from the enrollment's own
contract, so isNonContractEnrollment already does what exclude_b2b did.
Home goes back to the shared enrollments query.

This reverts commit a2205ed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(dashboard): describe b2b_contract_id as the enrollment's own contract

Since mitodl/mitxonline#4084 an enrollment's b2b_contract_id comes from
the enrollment, not its run. Update the filterEnrollmentsForContract
comment and the test names that still said otherwise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le (#4028)

* Infer the SEO title and description, overridable

The fields were plain optional text: blank meant the page head fell back
on the frontend, nothing else saw a resolved value, and #4009 then made
both compulsory -- so every author had to retype words the content
already had.

They are now inferred and overridable, the way Medium does it. The
columns are renamed to `seo_title_override` and
`seo_description_override`, and the serializer resolves `seo_title` and
`seo_description` read-only: the override where there is one, otherwise
the content's own title and the line under its headline. Storing the
override rather than the resolved value is the whole point -- rename an
article and its search title follows, until somebody decides otherwise.

The drawer shows what will be used as each field's placeholder rather
than pre-filling it, which is what keeps "unset" apart from "set to the
same words": pre-filled text would be saved as an override on the next
press and quietly stop tracking. Clearing an override hands the field
back to the content.

Required now means the *resolved* value is empty -- no override and
nothing to infer from, so an untitled draft or a body with no
subheading. Blank fields are the ordinary case and publish fine. That
also fixes a real bug in the resume path: it tested the overrides, so a
publish held back for topics never resumed once the SEO values came
from inference.

The counters measure the resolved value, since that is what a search
result will show.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address review on the inferred SEO fields

Three fixes from the review:

A subheading with any formatting in it was truncated to its first word.
ProseMirror splits a line on every mark boundary, so "A **complex**
article with *various* elements." is five text nodes and both helpers
read only the first -- resolving "A". Every inline node is now joined,
with nothing between them, since they are contiguous characters that
differ only by their marks. Fixed on both sides, because the API
resolves saved content and the editor infers from the document in hand.

The test factory could emit a response the serializer cannot produce: it
defaulted the resolved values independently of the overrides, so a
fixture supplying only `seo_description_override` claimed a blank
resolved description alongside it. They are now derived from whatever
overrides the caller passed, with an explicit resolved value still
winning.

Both fields were marked `required` whenever the rule was on, so a field
whose blank the save accepts -- because the content supplies a value --
still carried an asterisk and `aria-required`. Required now means what
the save means: only where nothing can stand in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address Chris's review: expand-then-contract, draft-saveable drawer

The migration is now the expand half of expand-then-contract. Deploys
are rolling, so for a few minutes old and new code run against the same
database: renaming the columns would take `seo_title` away from pods
still selecting it, and those 500 until they restart. 0010 adds the
override columns and backfills them, leaving the old pair in place for
the old code to keep reading. Dropping them is a separate PR once every
pod is on the new code. The legacy fields stay on the model for that
window, unread.

The drawer's save is no longer refused on a draft. A draft is worked on
in pieces -- topics now, a description once it is written -- and
refusing until everything resolves makes that impossible, which is not
what the requirement is for. The new `mustResolve` gate is true once the
content is public and while a publish is waiting on the drawer, and
false for a draft the editor simply opened; the asterisks follow it, so
no field claims to be required where the save accepts blank. The
publish-waiting case is what still stops a held-back press being saved
away.

Also: the two route comments no longer describe a fallback that lives in
the API now, two doc comments state their constraint rather than the
shape they replaced, and the misplaced test that had been inserted
between a comment and the test it documents is moved back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the old SEO columns out of this release's queries

Migrations run as a pre-deploy Job, so pods on the previous release query the
migrated schema until the rollout replaces them. Two gaps remained:

- AddField drops its default after backfilling, leaving the new columns NOT
  NULL with nothing to fall back on, so inserts from the previous release --
  which does not know the columns -- would fail. They get a db_default.
- Leaving `seo_title` and `seo_description` in the model meant this release
  kept selecting them, which only moved the breakage to the contracting PR:
  dropping the columns there would 500 every pod still on this release.

SeparateDatabaseAndState removes the two fields from Django's state while
leaving the columns in place, so this release stops referencing them and the
previous release can still read them. The columns also get a db_default, for
the same reason as the new ones. A later migration can then drop them with no
deployed code referring to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct three comments the SEO rework left behind

The publish gate's comment described falling back to the title and subheading
as the failure it guards against, when that fallback is the intended
behaviour and what it actually requires is a resolved value.

The length cap names `seo_title_override`, in its comment and in the test
that pins it. `seo_title` is the resolved, read-only field now, and off the
model entirely; the 255 characters are the override's.

`extractWebsiteContentDescription` returned absent rather than blank for
`getMetadataAsync` to default. The page head reads the resolved
`seo_description` from the API instead, and the one caller left turned the
absence straight back into a blank, so it returns the string it found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Ask only for the SEO field that cannot resolve

The two resolve separately, so most often only one of them holds a publish
back -- a piece of content with a title but no line under the headline
resolves its SEO title and not its description, which is the common case. The
section asked for both regardless, sending the author to a field that was
already fine and that the asterisks correctly left unmarked.

It also closed with an invitation to leave them blank, "each one follows your
article -- the title, and the line under the headline". For whichever field
is missing that is the opposite of the advice needed: blank is what is
blocking the publish, because the thing it would fall back to is the thing
the content does not have. Where something is missing the section now says
which, and why it cannot be inferred; the invitation stands where both
resolve and nothing is blocked.

Three tests pinned the old wording, each in a state where only the
description was missing -- one of them with a comment already noting that the
title is never missing here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@arbisoft.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* Show the API's 400 detail on enrollment surfaces

MITx Online answers a rejected enrollment with a 400 whose detail is
written for the learner, but Learn discarded the body and rendered the
same generic "please try again" for every failure — including ones that
cannot succeed on retry.

Adds badRequestDetail/badRequestDetailOr, which read a detail only from
a 400 and return undefined otherwise, so a 500 or a detail-less 400
keeps today's copy. The enroll hooks now expose the error object rather
than an isError boolean, and the enroll areas, header CTA, enrollment
dialog, and dashboard toasts prefer the server's string over their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* slim down chatty claude code comments

* Link users to support alongside enrollment error copy

An enrollment 400 detail is often only actionable by a human ("Error code:
CS_700"), but no enrollment error surface offered a way to reach support.

Add ErrorMessageWithSupport, which appends a "Contact Support" link to a
message, and use it on every enrollment failure surface: the product page
InfoBox and header alerts, both enrollment dialog alerts, and - via a new
opt-in `meta.contactSupport` read by the global mutation-error handler - the
dashboard enrollment toast.

The link points at the support site's request form (new urls.SUPPORT_REQUEST)
rather than the mailto: used by the dashboard's upgrade banners, which are
left as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@odlbot
odlbot requested a review from a team as a code owner October 7, 2026 11:13
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

OpenAPI Changes

128 changes: 0 error, 24 warning, 104 info

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants