Skip to content

feat(notifications): proxy Open edX notification preferences - #3897

Open
AhtishamShahid wants to merge 6 commits into
mainfrom
ahtisham/notification-preferences
Open

AhtishamShahid wants to merge 6 commits into
mainfrom
ahtisham/notification-preferences

Conversation

@AhtishamShahid

@AhtishamShahid AhtishamShahid commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Fixes mitodl/hq#12930

Paired with mitodl/mit-learn#3960, which is the UI. Merge this one first — without it, that PR's requests 404.

Description (What does it do?)

Open edX owns a learner's notification preferences and serves them only to the learner themselves, so nothing outside the LMS could read or change them on a learner's behalf.

This adds a GET/PUT /api/notification-preferences/ proxy that calls the LMS as the learner, using the Open edX bearer token this app already mints and refreshes for them. Nothing is stored here; the LMS response is passed through verbatim.

Three states are mapped to something a client can act on rather than a bare 502: a learner with no Open edX account yet gets 409, an LMS throttle passes through as 429, and any other upstream failure becomes 502. PUT validates the payload locally first, because the upstream API changes one channel per request and a malformed change would otherwise spend one of the learner's rate-limited LMS calls.

This PR is backend only. It originally carried a Notifications section for My Account; that UI now lives in MIT Learn (mitodl/mit-learn#3960), and the components, styles, query layer and constants were removed here.

How can this be tested?

  1. As a learner with a synced Open edX account, GET /api/notification-preferences/ and confirm the body matches what the LMS returns for that learner.
  2. PUT one change — for example {"notification_app": "discussion", "notification_type": "grouped_notification", "notification_channel": "web", "value": false} — then re-read and confirm it persisted, and that NotificationPreference in edx-platform changed for that user.
  3. PUT a malformed payload (a channel of email_cadence with no email_cadence field) and confirm a 400, with no LMS call made.
  4. As a learner with no Open edX account, confirm a 409 rather than a 500.
  5. Confirm the endpoint appears in openapi/specs/v0.yaml and that ./manage.py generate_openapi_spec leaves the committed specs unchanged.

Validation run on this branch: pytest openedx — 205 passed, 6 skipped; ruff clean; spec regeneration verified byte-identical with --fail-on-warn. Rebased onto current main (the earlier CI failures were staleness — a regenerated Keycloak dataclass and moved specs that main already had), and all checks pass.

Additional Context

The 200 body is declared in the schema as a pass-through object rather than a modelled shape: it is the LMS response, and pinning a shape here would misrepresent who owns it.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

OpenAPI Changes

Show/hide changes
## Changes for v0.yaml:
2 changes: 0 error, 0 warning, 2 info
info	[endpoint-added] at head/openapi/specs/v0.yaml
	in API GET /api/notification-preferences/
		endpoint added

info	[endpoint-added] at head/openapi/specs/v0.yaml
	in API PUT /api/notification-preferences/
		endpoint added



## Changes for v1.yaml:
2 changes: 0 error, 0 warning, 2 info
info	[endpoint-added] at head/openapi/specs/v1.yaml
	in API GET /api/notification-preferences/
		endpoint added

info	[endpoint-added] at head/openapi/specs/v1.yaml
	in API PUT /api/notification-preferences/
		endpoint added



## Changes for v2.yaml:
2 changes: 0 error, 0 warning, 2 info
info	[endpoint-added] at head/openapi/specs/v2.yaml
	in API GET /api/notification-preferences/
		endpoint added

info	[endpoint-added] at head/openapi/specs/v2.yaml
	in API PUT /api/notification-preferences/
		endpoint added



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

@AhtishamShahid
AhtishamShahid marked this pull request as draft August 28, 2026 11:01
@AhtishamShahid
AhtishamShahid requested a balanced review from Copilot August 28, 2026 11:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds learner-managed Open edX notification preferences to My Account through a session-authenticated proxy.

Changes:

  • Adds GET/PUT notification preference proxy endpoints and validation.
  • Adds notification controls, labels, styling, and LMS synchronization.
  • Adds backend and frontend tests for preferences and error handling.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
openedx/views.py Exposes the authenticated proxy view.
openedx/views_test.py Tests endpoint behavior and errors.
openedx/urls.py Registers the proxy route.
openedx/serializers.py Validates preference updates.
openedx/exceptions.py Defines the proxy exception.
openedx/api.py Calls Open edX using learner tokens.
openedx/api_test.py Tests LMS requests and authentication failures.
frontend/public/src/lib/queries/notificationPreferences.js Defines frontend query and mutation configurations.
frontend/public/src/containers/pages/settings/AccountSettingsPage.js Integrates preferences into My Account.
frontend/public/src/constants.js Adds notification display metadata.
frontend/public/src/components/NotificationPreferences.js Renders preference controls.
frontend/public/src/components/NotificationPreferences_test.js Tests preference rendering and interactions.
frontend/public/scss/notification-preferences.scss Styles the notification section.
frontend/public/scss/layout.scss Includes the new stylesheet.
Suppressed comments (2)

openedx/api.py:2063

  • A timeout/connection failure from requests.get escapes as an unhandled 500 because only non-200 responses are wrapped. Likewise, invalid JSON from a 200 response fails at resp.json(). Catch RequestException and JSON decoding errors and re-raise EdxApiNotificationPreferencesError so upstream transport failures follow the documented 502 path.
    resp = requests.get(
        edx_url(OPENEDX_NOTIFICATION_PREFERENCES_PATH),
        headers=_notification_preferences_headers(user),
        timeout=settings.EDX_API_CLIENT_TIMEOUT,
    )

openedx/api.py:2097

  • The PUT path also leaves connection timeouts and malformed successful JSON unwrapped, so the view returns 500 instead of the intended upstream 502. Convert request/decoding failures to EdxApiNotificationPreferencesError, matching the handling for non-200 LMS responses.
    resp = requests.put(
        edx_url(OPENEDX_NOTIFICATION_PREFERENCES_PATH),
        json=preference,
        headers={
            **_notification_preferences_headers(user),

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread openedx/api.py Outdated
Comment thread frontend/public/src/containers/pages/settings/AccountSettingsPage.js Outdated
Comment thread frontend/public/src/containers/pages/settings/AccountSettingsPage.js Outdated
Comment thread frontend/public/src/containers/pages/settings/AccountSettingsPage.js Outdated
@AhtishamShahid

AhtishamShahid commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favour of mitodl/mit-learn#3960

@AhtishamShahid
AhtishamShahid force-pushed the ahtisham/notification-preferences branch from 904e5a5 to b19e346 Compare September 21, 2026 13:22
@AhtishamShahid AhtishamShahid changed the title feat(notifications): manage Open edX notification preferences in My Account feat(notifications): proxy Open edX notification preferences Sep 21, 2026
@AhtishamShahid
AhtishamShahid force-pushed the ahtisham/notification-preferences branch from f8f1bc5 to fba828c Compare September 21, 2026 15:16
@AhtishamShahid
AhtishamShahid marked this pull request as ready for review September 21, 2026 17:08
@alexfigtree
alexfigtree self-requested a review September 23, 2026 13:29

@alexfigtree alexfigtree left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, your precommit is failing, I can take a better look once that has been fixed

…ccount

Adds a Notifications section to the account settings page, backed by a thin
proxy to the Open edX notification preferences API. Open edX remains the
source of truth for these preferences -- it is what decides whether a
notification is sent -- so nothing is stored locally.
The Open edX notifications drawer gear links to
/account-settings/#notifications (mitodl/hq#12930), so the section needs
that id for the link to land on it.
Object.entries is typed Array<[string, mixed]> in Flow's core lib, which
dropped PreferenceGroup to mixed and failed the CI flow check. Index the
dictionary with Object.keys instead so the value type survives.
- Only treat a genuinely unusable identity as the 409 "still being set up"
  state. A 400 from the token endpoint (revoked/invalid grant) stays a 409;
  a 5xx, bad client credentials or an incomplete OAuth handshake now surface
  as an upstream failure with the real status, so a synced learner gets an
  accurate retryable response instead of a permanent 409.
- Always render the anchored #notifications section, with loading, 409 and
  error notices, so the target the Open edX gear links to resolves even when
  there are no preferences to show.
- Honour the upstream show_preferences flag, which gates the whole feature.
- Only re-read after a successful PUT; re-reading on a 429 spent the throttle
  we had just been asked to back off from.
The notification settings UI now lives in MIT Learn, which calls this
endpoint rather than rendering its own section here. Removing the
components, styles, query layer and constants leaves this change as what
Learn actually consumes: the GET/PUT proxy onto Open edX's notification
preferences, with the learner's own LMS credentials.
…spec

The proxy was marked `@extend_schema(exclude=True)`, so it never reached
the generated spec and consumers had to hand-write a client for it.

Both methods are now annotated: the request serializer on PUT, the error
shapes for the states the view actually returns (409 with no courseware
account, 429 when the LMS throttles, 502 upstream), and the 200 body
declared as a pass-through object, since it is the LMS response verbatim
and is not ours to model.

Specs regenerated. Like the other unversioned /api/ paths, it appears in
v0, v1 and v2.
@AhtishamShahid
AhtishamShahid force-pushed the ahtisham/notification-preferences branch from eb3e173 to 83d0388 Compare September 23, 2026 17:50
Comment thread openedx/api.py
Comment on lines +2080 to +2090
headers=_notification_preferences_headers(user),
timeout=settings.EDX_API_CLIENT_TIMEOUT,
)

if resp.status_code != status.HTTP_200_OK:
raise EdxApiNotificationPreferencesError(
f"Error fetching Open edX notification preferences. {get_error_response_summary(resp)}", # noqa: EM102
status_code=resp.status_code,
)

return resp.json()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The functions get_notification_preferences and update_notification_preference do not handle network exceptions from the requests library, such as timeouts or connection errors.
Severity: MEDIUM

Suggested Fix

Wrap the requests calls within a try...except requests.exceptions.RequestException block. In the except block, catch the exception and raise an appropriate custom exception or return a response indicating a gateway error, similar to the pattern used elsewhere in the file.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: openedx/api.py#L2078-L2090

Potential issue: The functions `get_notification_preferences` and
`update_notification_preference` make API calls using the `requests` library but do not
handle potential network-level exceptions like `requests.exceptions.ConnectionError` or
`requests.exceptions.Timeout`. If the remote LMS is slow or unreachable, these unhandled
exceptions will propagate and cause a 500 Internal Server Error instead of the intended
502 Bad Gateway. Given the configured 60-second timeout (`EDX_API_CLIENT_TIMEOUT`), a
timeout exception is a realistic possibility in a production environment.

Did we get this right? 👍 / 👎 to inform future reviews.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants