Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- 6379:6379

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- name: update apt
run: sudo apt-get update -y
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
javascript-tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- name: Build the Docker image
env:
Expand Down Expand Up @@ -199,7 +199,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand All @@ -223,7 +223,7 @@ jobs:
GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v0
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down Expand Up @@ -262,7 +262,7 @@ jobs:
GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v1
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "^22.0.0"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/openapi-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout HEAD
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
ref: ${{ github.head_ref }}
path: head
- name: Checkout BASE
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
ref: ${{ github.base_ref }}
path: base
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand Down
13 changes: 13 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Release Notes
=============

Version 0.42.3
--------------

- ingest canvas html content (#2502)
- Fix userlist stale data (#2501)
- redirect org users to org dashboard (#2482)
- Set page size to 30 for the course query for programs in the org dash (#2494)
- Update dependency Django to v4.2.24 (#2496)
- adding fix for flaky test (#2491)
- Toggle NextJS image optimization with an env var (#2385)
- Stop running Sloan news/event tasks (#2493)
- Update actions/checkout digest to 08eba0b (#2454)

Version 0.42.2 (Released September 08, 2025)
--------------

Expand Down
41 changes: 36 additions & 5 deletions authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Authentication views"""

import logging
from urllib.parse import urljoin

from django.contrib.auth import logout
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.utils.text import slugify
from django.views import View

from main import settings
from main.middleware.apisix_user import ApisixUserMiddleware
from main.middleware.apisix_user import ApisixUserMiddleware, decode_apisix_headers

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,6 +66,8 @@ class CustomLoginView(View):
Redirect the user to the appropriate url after login
"""

header = "HTTP_X_USERINFO"

def get(
self,
request,
Expand All @@ -76,15 +80,42 @@ def get(
redirect_url = get_redirect_url(request)
if not request.user.is_anonymous:
profile = request.user.profile
if not profile.has_logged_in:
profile.has_logged_in = True
profile.save()
if (

apisix_header = decode_apisix_headers(request, self.header)

# Check if user belongs to any organizations
user_organizations = (
apisix_header.get("organizations", {}) if apisix_header else {}
)

if user_organizations:
# First-time login for org user: redirect to org dashboard
if not profile.has_logged_in:
first_org_name = next(iter(user_organizations.keys()))
org_slug = slugify(first_org_name)

log.info(
"User %s belongs to organization: %s (slug: %s)",
request.user.email,
first_org_name,
org_slug,
)

redirect_url = urljoin(
settings.APP_BASE_URL, f"/dashboard/organization/{org_slug}"
)
# Non-organization users: apply onboarding logic
elif (
not profile.completed_onboarding
and request.GET.get("skip_onboarding", "0") == "0"
):
params = urlencode({"next": redirect_url})
redirect_url = f"{settings.MITOL_NEW_USER_LOGIN_URL}?{params}"
profile.completed_onboarding = True
profile.save()

if not profile.has_logged_in:
profile.has_logged_in = True
profile.save()

return redirect(redirect_url)
79 changes: 72 additions & 7 deletions authentication/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import json
from base64 import b64encode
from unittest.mock import MagicMock
from urllib.parse import urljoin

import pytest
from django.conf import settings
from django.test import RequestFactory
from django.urls import reverse

Expand All @@ -28,10 +28,18 @@ def test_custom_login(mocker, next_url, allowed):
assert get_redirect_url(mock_request) == (next_url if allowed else "/app")


@pytest.mark.parametrize("has_apisix_header", [True, False])
@pytest.mark.parametrize("next_url", ["/search", None])
def test_logout(mocker, next_url, client, user, has_apisix_header):
@pytest.mark.parametrize(
"test_params",
[
(True, "/search"),
(True, None),
(False, "/search"),
(False, None),
],
)
def test_logout(mocker, client, user, test_params, settings):
"""User should be properly redirected and logged out"""
has_apisix_header, next_url = test_params
header_str = b64encode(
json.dumps(
{
Expand All @@ -55,10 +63,10 @@ def test_logout(mocker, next_url, client, user, has_apisix_header):
mock_logout.assert_called_once()


@pytest.mark.parametrize("is_authenticated", [True])
@pytest.mark.parametrize("has_next", [False])
def test_next_logout(mocker, client, user, is_authenticated, has_next):
@pytest.mark.parametrize("test_params", [(True, False)])
def test_next_logout(mocker, client, user, test_params, settings):
"""Test logout redirect cache assignment"""
is_authenticated, has_next = test_params
next_url = "https://ocw.mit.edu"
mock_request = mocker.MagicMock(
GET={"next": next_url if has_next else None},
Expand Down Expand Up @@ -211,3 +219,60 @@ def test_custom_login_view_first_time_login_sets_has_logged_in(mocker):

# Verify redirect was called with the correct URL
mock_redirect.assert_called_once_with("/dashboard")


@pytest.mark.parametrize(
"test_case",
[
(
(False, False),
"/dashboard/organization/test-organization",
), # First-time login → org dashboard
(
(False, True),
"/dashboard/organization/test-organization",
), # First-time login → org dashboard
((True, False), "/app"), # Subsequent login → normal app (not onboarding!)
((True, True), "/app"), # Subsequent login → normal app
],
)
def test_login_org_user_redirect(mocker, client, user, test_case, settings):
"""Test organization user redirect behavior - org users skip onboarding regardless of onboarding status"""
# Unpack test case
profile_state, expected_url = test_case
has_logged_in, completed_onboarding = profile_state

# Set up user profile based on test scenario
user.profile.has_logged_in = has_logged_in
user.profile.completed_onboarding = completed_onboarding
user.profile.save()

header_str = b64encode(
json.dumps(
{
"preferred_username": user.username,
"email": user.email,
"sub": user.global_id,
"organization": {
"Test Organization": {
"role": "member",
"id": "org-123",
}
},
}
).encode()
)
client.force_login(user)
response = client.get(
"/login/",
follow=False,
HTTP_X_USERINFO=header_str,
)
assert response.status_code == 302
# Handle environment differences - in some envs it returns full URL, in others just path
expected_full_url = urljoin(settings.APP_BASE_URL, expected_url)
assert response.url in [expected_url, expected_full_url]

# Verify that org users are never sent to onboarding
# (onboarding URL would contain settings.MITOL_NEW_USER_LOGIN_URL)
assert settings.MITOL_NEW_USER_LOGIN_URL not in response.url
1 change: 1 addition & 0 deletions env/backend.env
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ KEYCLOAK_CLIENT_ID=apisix
KEYCLOAK_CLIENT_SECRET=HckCZXToXfaetbBx0Fo3xbjnC468oMi4 # pragma: allowlist-secret
KEYCLOAK_DISCOVERY_URL=http://kc.ol.local:8066/realms/ol-local/.well-known/openid-configuration
KEYCLOAK_REALM_NAME=ol-local
# For some organization related functionality, add organization:* if you have orgs enabled
KEYCLOAK_SCOPES="openid profile ol-profile"
KEYCLOAK_SVC_KEYSTORE_PASSWORD=supertopsecret1234
KEYCLOAK_SVC_HOSTNAME=kc.ol.local
Expand Down
1 change: 1 addition & 0 deletions env/backend.local.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ KEYCLOAK_CLIENT_ID=apisix
KEYCLOAK_CLIENT_SECRET=HckCZXToXfaetbBx0Fo3xbjnC468oMi4 # pragma: allowlist-secret
KEYCLOAK_DISCOVERY_URL=http://kc.ol.local:8066/realms/ol-local/.well-known/openid-configuration
KEYCLOAK_REALM_NAME=ol-local
# For some organization related functionality, add organization:* if you have orgs enabled
KEYCLOAK_SCOPES="openid profile ol-profile"
KEYCLOAK_SVC_KEYSTORE_PASSWORD=supertopsecret1234
KEYCLOAK_SVC_HOSTNAME=kc.ol.local
Expand Down
1 change: 1 addition & 0 deletions env/frontend.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NODE_ENV=development
PORT=8062
SENTRY_ENV=dev # Re-enable sentry
OPTIMIZE_IMAGES="true"

# Environment variables with `NEXT_PUBLIC_` prefix are exposed to the client side
NEXT_PUBLIC_ORIGIN=${MITOL_APP_BASE_URL}
Expand Down
24 changes: 20 additions & 4 deletions frontends/api/src/hooks/learningResources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,16 @@ const useLearningResourceSetUserListRelationships = () => {
params: LearningResourcesApiLearningResourcesUserlistsPartialUpdateRequest,
) => learningResourcesApi.learningResourcesUserlistsPartialUpdate(params),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: userlistKeys.membershipList() })
/**
* We need to invalidate:
* - membership check
* - list of user lists (count has changed)
* - userlist detail annd listing for any lists we modified
*
* That's a lot. Let's just invalidate root.
* Additionally, the lists we've removed from the resource are not easily available.
*/
queryClient.invalidateQueries({ queryKey: userlistKeys.root })
},
})
}
Expand All @@ -110,9 +119,16 @@ const useLearningResourceSetLearningPathRelationships = () => {
) =>
learningResourcesApi.learningResourcesLearningPathsPartialUpdate(params),
onSettled: () => {
queryClient.invalidateQueries({
queryKey: learningPathKeys.membershipList(),
})
/**
* We need to invalidate:
* - membership check
* - list of user lists (count has changed)
* - userlist detail annd listing for any lists we modified
*
* That's a lot. Let's just invalidate root.
* Additionally, the lists we've removed from the resource are not easily available.
*/
queryClient.invalidateQueries({ queryKey: learningPathKeys.root })
},
})
}
Expand Down
5 changes: 5 additions & 0 deletions frontends/main/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ const { validateEnv } = require("./validateEnv")

validateEnv()

const OPTIMIZE_IMAGES = Boolean(
(process.env.OPTIMIZE_IMAGES ?? "true") === "true",
)

const processFeatureFlags = () => {
const featureFlagPrefix =
process.env.NEXT_PUBLIC_POSTHOG_FEATURE_PREFIX || "FEATURE_"
Expand Down Expand Up @@ -90,6 +94,7 @@ const nextConfig = {
transpilePackages: ["@mitodl/smoot-design/ai"],

images: {
unoptimized: !OPTIMIZE_IMAGES,
remotePatterns: [
{
hostname: "**",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,21 @@ const setupProgramsAndCourses = () => {
{ results: [programB] },
)
setMockResponse.get(
urls.courses.coursesList({ id: programA.courses, org_id: orgX.id }),
urls.courses.coursesList({
id: programA.courses,
org_id: orgX.id,
page_size: 30,
}),
{
results: coursesA.results,
},
)
setMockResponse.get(
urls.courses.coursesList({ id: programB.courses, org_id: orgX.id }),
urls.courses.coursesList({
id: programB.courses,
org_id: orgX.id,
page_size: 30,
}),
{
results: coursesB.results,
},
Expand Down Expand Up @@ -262,6 +270,7 @@ function setupOrgDashboardMocks(
mitxonline.urls.courses.coursesList({
id: program.courses,
org_id: org.id,
page_size: 30,
}),
{ results: courses },
)
Expand Down
Loading
Loading