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
710 changes: 355 additions & 355 deletions .yarn/releases/yarn-4.9.2.cjs → .yarn/releases/yarn-4.10.3.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ enableGlobalCache: false

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.9.2.cjs
yarnPath: .yarn/releases/yarn-4.10.3.cjs

# https://github.com/vitejs/vite-plugin-react-swc/issues/74#issuecomment-1520484130
# https://github.com/swc-project/swc/issues/5616#issuecomment-1265639797
Expand Down
20 changes: 20 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
Release Notes
=============

Version 0.46.0
--------------

- bump course search utils (#2615)
- use v2 enrollments, hide org enrollments in my learning (#2604)
- Redirect to dashboard home after login, usually (#2600)
- left align dashboard tab titles (#2612)
- Padding to fix background position (#2603)
- adding ecosystems subtopic (#2602)
- chore(deps): update dependency ruff to v0.14.1 (#2607)
- chore(deps): update yarn to v4.10.3 (#2609)
- chore(deps): update node.js to v22.20.0 (#2608)
- fix(deps): update dependency @faker-js/faker to v10 (#2470)
- [pre-commit.ci] pre-commit autoupdate (#2315)
- Organization landing route (#2588)
- Video section text change (#2601)
- fix(deps): update dependency django to v4.2.25 [security] (#2561)
- MIT CLimate Portal ETL (#2589)
- fix(deps): update dependency django-oauth-toolkit to v3 (#1816)

Version 0.45.8 (Released October 16, 2025)
--------------

Expand Down
2 changes: 1 addition & 1 deletion articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def to_internal_value(self, data):
return clean_html(data)


class ArticleSerializer(serializers.ModelSerializer):
class RichTextArticleSerializer(serializers.ModelSerializer):
"""
Serializer for LearningResourceInstructor model
"""
Expand Down
4 changes: 2 additions & 2 deletions articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework.permissions import IsAdminUser

from articles.models import Article
from articles.serializers import ArticleSerializer
from articles.serializers import RichTextArticleSerializer
from main.constants import VALID_HTTP_METHODS
from main.utils import cache_page_for_all_users, clear_search_cache

Expand Down Expand Up @@ -34,7 +34,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
Viewset for Article viewing and editing.
"""

serializer_class = ArticleSerializer
serializer_class = RichTextArticleSerializer
queryset = Article.objects.all()
pagination_class = DefaultPagination

Expand Down
32 changes: 8 additions & 24 deletions authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
"""Authentication views"""

import logging
from urllib.parse import urljoin

from django.conf import settings
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, decode_apisix_headers

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,6 +78,7 @@ def get(
"""
redirect_url = get_redirect_url(request, ["next"])
signup_redirect_url = get_redirect_url(request, ["signup_next", "next"])
should_skip_onboarding = request.GET.get("skip_onboarding", "0") != "0"
if not request.user.is_anonymous:
profile = request.user.profile

Expand All @@ -91,31 +90,16 @@ def get(
)

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}"
)
# first-time non-org users
elif not profile.has_logged_in:
if request.GET.get("skip_onboarding", "0") == "0":
should_skip_onboarding = True

if not profile.has_logged_in:
if should_skip_onboarding:
redirect_url = signup_redirect_url
else:
params = urlencode({"next": signup_redirect_url})
redirect_url = f"{settings.MITOL_NEW_USER_LOGIN_URL}?{params}"
profile.save()
else:
redirect_url = signup_redirect_url

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

Expand Down
65 changes: 44 additions & 21 deletions authentication/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
from base64 import b64encode
from typing import NamedTuple
from unittest.mock import MagicMock
from urllib.parse import urljoin

Expand Down Expand Up @@ -29,7 +30,7 @@
(["allowed-2"], "https://good.com/url-2"),
],
)
def test_get_redirect_url(mocker, param_names, expected_redirect):
def test_get_redirect_url(mocker, param_names, expected_redirect, settings):
"""Next url should be respected if host is allowed"""
GET = {
"exists-a": "/url-a",
Expand All @@ -38,10 +39,7 @@ def test_get_redirect_url(mocker, param_names, expected_redirect):
"disallowed-a": "https://malicious.com/url-1",
"allowed-2": "https://good.com/url-2",
}
mocker.patch(
"authentication.views.settings.ALLOWED_REDIRECT_HOSTS",
["good.com"],
)
settings.ALLOWED_REDIRECT_HOSTS = ["good.com"]

mock_request = mocker.MagicMock(GET=GET)
assert get_redirect_url(mock_request, param_names) == expected_redirect
Expand All @@ -50,6 +48,7 @@ def test_get_redirect_url(mocker, param_names, expected_redirect):
@pytest.mark.parametrize(
"test_params",
[
# has_apisix_header, next_url
(True, "/search"),
(True, None),
(False, "/search"),
Expand Down Expand Up @@ -129,8 +128,9 @@ def test_next_logout(mocker, client, user, test_params, settings):

@pytest.mark.parametrize("is_authenticated", [True, False])
@pytest.mark.parametrize("has_next", [True, False])
def test_custom_logout_view(mocker, client, user, is_authenticated, has_next):
def test_custom_logout_view(mocker, client, user, is_authenticated, has_next, settings): # noqa: PLR0913
"""Test logout redirect"""
settings.ALLOWED_REDIRECT_HOSTS = ["ocw.mit.edu"]
next_url = "https://ocw.mit.edu" if has_next else ""
mock_request = mocker.MagicMock(user=user, META={})
if is_authenticated:
Expand Down Expand Up @@ -245,23 +245,43 @@ def test_custom_login_view_first_time_login_sets_has_logged_in(mocker):
mock_profile.save.assert_called_once()


class LoginOrgUserRedirectParams(NamedTuple):
"""Parameters for testing org user login redirect behavior"""

has_logged_in: bool
login_url: str
expected_redirect: str


@pytest.mark.parametrize(
"test_case",
"params",
[
(
False,
"/dashboard/organization/test-organization",
), # First-time login → org dashboard
(
True,
"/app",
), # Subsequent login → normal app
LoginOrgUserRedirectParams(
has_logged_in=False,
login_url="/login/?next=/dashboard",
expected_redirect="/dashboard",
),
LoginOrgUserRedirectParams(
has_logged_in=False,
login_url="/login/?next=/dashboard&signup_next=/somewhere-else",
expected_redirect="/somewhere-else",
),
LoginOrgUserRedirectParams(
has_logged_in=True,
login_url="/login/?next=/dashboard&signup_next=/somewhere-else",
expected_redirect="/dashboard",
),
],
)
def test_login_org_user_redirect(mocker, client, user, test_case, settings):
def test_login_org_user_redirect(
mocker,
client,
user,
params,
settings,
):
"""Test organization user redirect behavior - org users skip onboarding regardless of login history"""
# Unpack test case
has_logged_in, expected_url = test_case
has_logged_in, login_url, expected_redirect = params

# Set up user profile based on test scenario
user.profile.has_logged_in = has_logged_in
Expand All @@ -284,15 +304,18 @@ def test_login_org_user_redirect(mocker, client, user, test_case, settings):
)
client.force_login(user)
response = client.get(
"/login/",
login_url,
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]
expected_full_redirect = urljoin(settings.APP_BASE_URL, expected_redirect)
assert response.url in [expected_redirect, expected_full_redirect]

# 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

user.profile.refresh_from_db()
assert user.profile.has_logged_in is True
17 changes: 17 additions & 0 deletions data_fixtures/fixtures/offered_by.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@
"updated_on": "2023-08-08T14:27:25.128989+00:00"
}
},
{
"model": "learning_resources.LearningResourceOfferor",
"fields": {
"name": "MIT Climate Portal",
"code": "climate",
"professional": false,
"offerings": ["Learning Materials"],
"audience": ["Everyone"],
"formats": ["Online", "Downloadable"],
"fee": ["Free"],
"certifications": ["No Certificates"],
"content_types": ["Academic"],
"more_information": "https://climate.mit.edu",
"created_on": "2023-08-08T14:27:25.128989+00:00",
"updated_on": "2023-08-08T14:27:25.128989+00:00"
}
},
{
"model": "learning_resources.LearningResourceOfferor",
"fields": {
Expand Down
44 changes: 44 additions & 0 deletions data_fixtures/migrations/0018_add_mit_climate_platform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated manually


from django.db import migrations


def remove_climate_platform(apps, schema_editor):
"""
Remove canvas platform
"""

LearningResourcePlatform = apps.get_model(
"learning_resources", "LearningResourcePlatform"
)
LearningResourcePlatform.objects.filter(code="climate").delete()


def add_climate_platform(apps, schema_editor):
LearningResourcePlatform = apps.get_model(
"learning_resources", "LearningResourcePlatform"
)

LearningResourcePlatform.objects.update_or_create(
code="climate",
defaults={
"name": "MIT Climate",
"is_edx": False,
"has_content_files": False,
"url": "https://climate.mit.edu/",
},
)


class Migration(migrations.Migration):
dependencies = [
(
"data_fixtures",
"0017_update_edx_content_urls",
),
]

operations = [
migrations.RunPython(add_climate_platform, remove_climate_platform),
]
45 changes: 45 additions & 0 deletions data_fixtures/migrations/0019_add_mit_climate_offerer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.2.24 on 2025-10-07 17:07

from django.db import migrations


def remove_climate_offeror(apps, schema_editor):
"""
Remove canvas platform
"""

LearningResourceOfferor = apps.get_model(
"learning_resources", "LearningResourceOfferor"
)
LearningResourceOfferor.objects.filter(code="climate").delete()


def add_climate_offeror(apps, schema_editor):
LearningResourceOfferor = apps.get_model(
"learning_resources", "LearningResourceOfferor"
)

LearningResourceOfferor.objects.update_or_create(
code="climate",
defaults={
"name": "MIT Climate Portal",
"professional": False,
"offerings": ["Learning Materials"],
"audience": ["Everyone"],
"formats": ["Online", "Downloadable"],
"fee": ["Free"],
"certifications": ["No Certificates"],
"content_types": ["Academic"],
"more_information": "https://climate.mit.edu",
},
)


class Migration(migrations.Migration):
dependencies = [
("data_fixtures", "0018_add_mit_climate_platform"),
]

operations = [
migrations.RunPython(add_climate_offeror, remove_climate_offeror),
]
Loading
Loading