feat(notifications): proxy Open edX notification preferences - #3897
AhtishamShahid wants to merge 6 commits into
Conversation
OpenAPI ChangesShow/hide changesUnexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
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.getescapes as an unhandled 500 because only non-200 responses are wrapped. Likewise, invalid JSON from a 200 response fails atresp.json(). CatchRequestExceptionand JSON decoding errors and re-raiseEdxApiNotificationPreferencesErrorso 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.
|
|
904e5a5 to
b19e346
Compare
f8f1bc5 to
fba828c
Compare
alexfigtree
left a comment
There was a problem hiding this comment.
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.
eb3e173 to
83d0388
Compare
| 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() |
There was a problem hiding this comment.
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.
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.
PUTvalidates 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?
GET /api/notification-preferences/and confirm the body matches what the LMS returns for that learner.PUTone change — for example{"notification_app": "discussion", "notification_type": "grouped_notification", "notification_channel": "web", "value": false}— then re-read and confirm it persisted, and thatNotificationPreferencein edx-platform changed for that user.PUTa malformed payload (a channel ofemail_cadencewith noemail_cadencefield) and confirm a 400, with no LMS call made.openapi/specs/v0.yamland that./manage.py generate_openapi_specleaves the committed specs unchanged.Validation run on this branch:
pytest openedx— 205 passed, 6 skipped;ruffclean; spec regeneration verified byte-identical with--fail-on-warn. Rebased onto currentmain(the earlier CI failures were staleness — a regenerated Keycloak dataclass and moved specs thatmainalready 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.