Skip to content

Release 0.81.3 - #4013

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

odlbot wants to merge 11 commits into
releasefrom
release-candidate

Conversation

@odlbot

@odlbot odlbot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Shankar Ambady

Matt Bertrand

Tobias Macey

Carey P Gumaer

Ahtesham Quraish

Sar

mbertrand and others added 11 commits September 29, 2026 11:15
* fix(widgets): remove the unused RSS Feed widget type

Per team confirmation on hq#13473, the RSS Feed widget was a
channels/Open-Discussions-era feature with no current way to add one
-- verified independently: the widget-list feature's frontend surface
(hooks/widget_lists) has no consuming component anywhere in the
frontend, so no widget type, RSS included, is reachable through any
current UI. The backend registry (get_widget_classes) still fully
accepted and processed RSS widgets via the API, though, including its
unvalidated server-side feedparser.parse(url) fetch -- the SSRF
reported in hq#13473.

Rather than adding an SSRF allow-list to a feature nobody can reach or
create, remove it outright:
- widgets/serializers/rss.py and its tests (_fetch_rss,
  RssFeedWidgetConfigSerializer, RssFeedWidgetSerializer)
- the registry entry in widgets/serializers/utils.py
  (get_widget_classes/get_widget_type_mapping/get_widget_type_names
  now only expose Markdown/URL/People)
- the now-dead WIDGETS_RSS_CACHE_TTL setting (no env var override
  anywhere in mit-learn or ol-infrastructure)
- the type_rss factory trait and the RSS entry in the available-widgets
  test fixture
- regenerated openapi/specs/v0.yaml and the v0 TypeScript client to
  drop "RSS Feed" from the widget_type enum

Markdown/URL/People widget types are untouched -- this is scoped to
the specific dead, vulnerable feature this issue is about, not the
broader (also currently frontend-unreachable) widget-list feature.

Fixes mitodl/hq#13473

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

* fix(openapi): acknowledge the intentional RSS Feed enum removal

oasdiff correctly flags removing "RSS Feed" from widget_type's enum as
a breaking change for PATCH/PUT /api/v0/widget_lists/{id}/ -- which is
the intended effect here, not an oversight. Verified locally with the
same tufin/oasdiff invocation the CI job runs (base=origin/main,
head=this branch): passes with exit 0 once these entries are added.

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

* fix(widgets): reject unsupported widget_type in update() instead of crashing

Sentry review feedback on this PR: WidgetListSerializer.update() looked
up the serializer class for a widget's widget_type and called it
unconditionally. If the type isn't in the registry -- which, after
this PR, "RSS Feed" now isn't -- the lookup returns None and calling
it raises an unhandled TypeError: 'NoneType' object is not callable,
a 500 instead of a clean validation error.

Confirmed this is a pre-existing bug in update() (the read path,
get_widgets, already guarded against unknown widget_type; update()
never did), not something specific to RSS -- reproduced the exact
TypeError directly against the current registry to confirm. This PR's
removal of "RSS Feed" from the registry just makes it newly reachable
for a value that used to be valid, so any client still sending it
would now hit this crash instead of a clean rejection.

Raise ValidationError instead when the widget_type has no matching
serializer class (covers missing types too, not just unsupported
ones). New parametrized test covers an unsupported string, "RSS Feed"
specifically, and a missing widget_type key.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…entFileViewSet (#3999)

ContentFileViewSet.private_fields only listed "content", so anonymous
and non-privileged callers could read a ContentFile's LLM-generated
.summary and .flashcards -- a full AI-condensed summary and ready-made
study flashcards distilled from the same gated course material --
even though .content itself was correctly hidden. 1,823 of 191,818
rows in QA already have populated values being served this way today.

summary/flashcards were added to the model in March 2025; the
private_fields=["content"] gate was added afterward in June 2025 and
just never included them. Derive private_fields from the existing
CONTENT_FILE_LARGE_FIELDS constant (content, summary, flashcards) --
which already groups these three as equivalent-sensitivity elsewhere
in the codebase -- instead of a separate hardcoded list, so they can't
drift apart again the next time an LLM-derived field is added.

Fixes mitodl/hq#13461

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ublished control bar (#4009)

* Lay the published control bar out like the edit one

It is the same bar in the same place, but the two looked unrelated: the
published view ran the full width of the window with its buttons against
the left edge and the status against the right, while edit mode puts the
status at the left end and the actions at the right, both lined up with
the breadcrumb and the text.

The published view now reuses the same `ActionRow`, so it carries the
article's 890px column and cannot drift from edit mode when that column
changes.

Also drops the flex wrapper around the read-only toolbar. It set gap and
margin on a `position: fixed` child, which it could not lay out either
way, and now that the button group uses the same container inside the
row its presence there was actively misleading.

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

* Require an SEO title and description before publishing

They are what a search result and a link preview show, and without them
the page head falls back to the title and whatever the body happens to
open with -- which is the thin description the SEO rules single out. So
publishing now insists on both: the press opens the settings drawer
instead, and resumes once they are written.

Held to the same rule as topics, and implemented the same way, for the
same reason -- it has to be the publish and not the save, because a
draft writes itself every couple of seconds and autosave cannot stop to
ask. Unlike topics this applies to news as well: news has no topics
section, so the SEO fields are the only thing its publish can wait on.

The drawer refuses to save while a publish is waiting on it and still
has not got what it needs. Saving closes the drawer and closing forgets
the press, so allowing it would drop the publish with nothing on screen
to say why -- a hole the topics rule had too.

`awaitingTopicsForPublish` becomes `awaitingSettingsForPublish`, since it
now covers either requirement, and the resume tests what the drawer just
handed over rather than state that has not landed. `!topicsRequired` in
that test is what keeps news from stranding: its drawer sends no `topics`
at all, so a bare length check would never resume.

Messages key on whether the content is published rather than on whether
the save is refused. The two used to coincide and no longer do -- a
draft's save is refused too while a publish waits -- and telling a draft
it is published would simply be wrong.

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

* Refuse the settings save while a required field is blank

The drawer marked both SEO fields required, put an asterisk on each
label, and said an SEO title and description were needed to publish --
then let Save Settings through with both empty. Same for topics.

The refusal now follows the requirement, on a draft as much as on
something public. That collapses `topicsMayNotBeEmptied` and
`seoMayNotBeEmptied` into `topicsRequired` and `seoRequired`: the two
were always going to agree, and a second prop that only ever restated
the first was the reason the button and the label could disagree in the
first place. `contentIsPublished` stays, since it still picks which
sentence a section shows.

A draft's *content* is untouched by this -- autosave keeps writing it,
and only the drawer's own settings wait on being complete.

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>
* Fall back to the original image before the default image

The image optimizer fetches remote images server-side, and some hosts
block that (e.g. bot protection answering 403) while still serving the
same image to browsers. useImageWithFallback went straight from the
failed optimized image to the default image, so those resources lost
their images. It now retries the original with next/image's
`unoptimized` first, and only falls back to the default if that fails
too.

Every image using the hook passes the new `unoptimized` value through.
Also type byImageSrc's error functions with their real arguments so the
queries accept the `nextJsOriginalSrc` option in TypeScript.

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

* Skip the original-image retry when the image was already unoptimized

Next.js serves some images unoptimized regardless of the prop, e.g.
SVGs and data: URLs. For those, the "original" retry rendered the same
<img>, no second error fired, and the image stayed broken instead of
falling back to the default. onError now checks the failed element's
src and goes straight to the fallback when it was already the original.

Also move VideoCard's hand-rolled optimized-then-placeholder fallback
onto useImageWithFallback so its thumbnails get the original-image retry.

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

* Derive the fallback stage from src during render

Replace the useEffect that reset the stage when src changed with state
that records which src failed. Any other src starts at its initial
stage in the same render, so a new src can no longer render once with
the previous src's stage (e.g. loading unoptimized, or twice). Adds a
test that records every render to check this.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(warehouse): read the warehouse catalog and schema from settings

Every BaseWarehouseETLTask pinned its view to
ol_data_lake_production.ol_warehouse_production_integrations, so RC had no
way to run a warehouse-pull task against QA data. Tasks now declare a bare
table_name and view_name composes it with WAREHOUSE_CATALOG and
WAREHOUSE_SCHEMA, which default to the production pair.

Each part is checked on its own because a dotted setting would still pass
iter_rows's check once joined, but address a different catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa1mSBw5fudATLND9pAqTM

* fix(warehouse): reject subclasses that pin view_name

A subclass assigning view_name as a class attribute shadows the property and
skips WAREHOUSE_CATALOG/WAREHOUSE_SCHEMA without any error, which is the
pattern the open ownership-guard PR and the closed Cohort 1 task PR both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fa1mSBw5fudATLND9pAqTM

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* adding initial working implementation

* styling changes

* fix tests

* style fixes

* prompt cleanup

* fix tests

* start fresh thread on first message and update prompt

* make summary prompt configurable/overridable via env var

* Refactor environment variable handling in tests

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor response retrieval and error handling

Refactor response handling to check for error messages and improve readability.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Stabilize frontend tests for CI

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

* Restore unsubscribe authorization guard

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

* Fix CarouselV2 test lint import

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

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

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

* Stabilize product page React tests

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

* Stabilize async frontend tests

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

* Fix CoursePage test formatting

Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>

* remove number badges

* moving prompt to learn-ai and using separate route for search summary agent

* add timeout for test

* Update frontends/main/src/page-components/SearchDisplay/AiSearchOverview.tsx

Co-authored-by: Matt Bertrand <mrbertrand@gmail.com>

* fix test

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: shanbady <196425+shanbady@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Bertrand <mrbertrand@gmail.com>
@odlbot
odlbot requested a review from a team as a code owner September 29, 2026 19:05
@github-actions

Copy link
Copy Markdown

OpenAPI Changes

9 changes: 6 error, 0 warning, 3 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.

7 participants