Skip to content

Commit 01ece7a

Browse files
authored
redirect new users to onboarding (#2204)
* adding initial changes for redirect on backend * check for next url post-onboarding * adding settings and field changes * adding test and redirect post onboarding * set onboarding flag to true for existing users * removing log * switching conditional * fixing redirect logic * fixing testcase
1 parent 938c265 commit 01ece7a

File tree

8 files changed

+162
-7
lines changed

8 files changed

+162
-7
lines changed

authentication/views.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from django.contrib.auth import logout
66
from django.shortcuts import redirect
7-
from django.utils.http import url_has_allowed_host_and_scheme
7+
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
88
from django.views import View
99

1010
from main import settings
@@ -73,4 +73,15 @@ def get(
7373
"""
7474
GET endpoint for logging a user in.
7575
"""
76-
return redirect(get_redirect_url(request))
76+
redirect_url = get_redirect_url(request)
77+
if not request.user.is_anonymous:
78+
profile = request.user.profile
79+
if (
80+
not profile.completed_onboarding
81+
and request.GET.get("skip_onboarding", "0") == "0"
82+
):
83+
params = urlencode({"next": redirect_url})
84+
redirect_url = f"{settings.MITOL_NEW_USER_LOGIN_URL}?{params}"
85+
profile.completed_onboarding = True
86+
profile.save()
87+
return redirect(redirect_url)

authentication/views_test.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import json
44
from base64 import b64encode
5+
from unittest.mock import MagicMock
56

67
import pytest
78
from django.conf import settings
9+
from django.test import RequestFactory
10+
from django.urls import reverse
811

9-
from authentication.views import get_redirect_url
12+
from authentication.views import CustomLoginView, get_redirect_url
1013

1114

1215
@pytest.mark.parametrize(
@@ -108,3 +111,67 @@ def test_custom_logout_view(mocker, client, user, is_authenticated, has_next):
108111
client.force_login(user)
109112
resp = client.get(f"/logout/?next={next_url}", request=mock_request)
110113
assert resp.url == (next_url if has_next else "/app")
114+
115+
116+
def test_custom_login_view_authenticated_user_with_onboarding(mocker):
117+
"""Test CustomLoginView for an authenticated user with incomplete onboarding"""
118+
factory = RequestFactory()
119+
request = factory.get(reverse("login"), {"next": "/dashboard"})
120+
request.user = MagicMock(is_anonymous=False)
121+
request.user.profile = MagicMock(completed_onboarding=False)
122+
mocker.patch("authentication.views.get_redirect_url", return_value="/dashboard")
123+
mocker.patch("authentication.views.urlencode", return_value="next=/dashboard")
124+
mocker.patch(
125+
"authentication.views.settings.MITOL_NEW_USER_LOGIN_URL", "/onboarding"
126+
)
127+
128+
response = CustomLoginView().get(request)
129+
130+
assert response.status_code == 302
131+
assert response.url == "/onboarding?next=/dashboard"
132+
133+
134+
def test_custom_login_view_authenticated_user_skip_onboarding(mocker):
135+
"""Test skip_onboarding flag skips redirect to onboarding and sets completed_onboarding"""
136+
factory = RequestFactory()
137+
request = factory.get(
138+
reverse("login"), {"next": "/dashboard", "skip_onboarding": "1"}
139+
)
140+
request.user = MagicMock(is_anonymous=False)
141+
request.user.profile = MagicMock(completed_onboarding=False)
142+
mocker.patch("authentication.views.get_redirect_url", return_value="/dashboard")
143+
144+
response = CustomLoginView().get(request)
145+
request.user.profile.refresh_from_db()
146+
# user should not be marked as completed onboarding
147+
assert request.user.profile.completed_onboarding is False
148+
149+
assert response.status_code == 302
150+
assert response.url == "/dashboard"
151+
152+
153+
def test_custom_login_view_authenticated_user_with_completed_onboarding(mocker):
154+
"""Test test that user who has completed onboarding is redirected to next url"""
155+
factory = RequestFactory()
156+
request = factory.get(reverse("login"), {"next": "/dashboard"})
157+
request.user = MagicMock(is_anonymous=False)
158+
request.user.profile = MagicMock(completed_onboarding=True)
159+
mocker.patch("authentication.views.get_redirect_url", return_value="/dashboard")
160+
161+
response = CustomLoginView().get(request)
162+
163+
assert response.status_code == 302
164+
assert response.url == "/dashboard"
165+
166+
167+
def test_custom_login_view_anonymous_user(mocker):
168+
"""Test redirect for anonymous user"""
169+
factory = RequestFactory()
170+
request = factory.get(reverse("login"), {"next": "/dashboard"})
171+
request.user = MagicMock(is_anonymous=True)
172+
mocker.patch("authentication.views.get_redirect_url", return_value="/dashboard")
173+
174+
response = CustomLoginView().get(request)
175+
176+
assert response.status_code == 302
177+
assert response.url == "/dashboard"

frontends/main/src/app-pages/OnboardingPage/OnboardingPage.test.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { merge, times } from "lodash"
44
import {
55
renderWithProviders,
66
screen,
7-
waitFor,
87
setMockResponse,
98
user,
109
} from "../../test-utils"
@@ -19,8 +18,32 @@ import {
1918
type Profile,
2019
} from "api/v0"
2120

21+
import { waitFor } from "@testing-library/react"
22+
2223
import OnboardingPage from "./OnboardingPage"
2324

25+
const oldWindowLocation = window.location
26+
27+
beforeAll(() => {
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
delete (window as any).location
30+
31+
window.location = Object.defineProperties(
32+
{} as unknown as string & Location,
33+
{
34+
...Object.getOwnPropertyDescriptors(oldWindowLocation),
35+
assign: {
36+
configurable: true,
37+
value: jest.fn(),
38+
},
39+
},
40+
)
41+
})
42+
43+
afterAll(() => {
44+
window.location = oldWindowLocation as unknown as string & Location
45+
})
46+
2447
const STEPS_DATA: Partial<Profile>[] = [
2548
{
2649
topic_interests: [factories.learningResources.topic()],
@@ -68,7 +91,9 @@ const setup = async (profile: Profile) => {
6891
...req,
6992
}))
7093

71-
renderWithProviders(<OnboardingPage />)
94+
renderWithProviders(<OnboardingPage />, {
95+
url: "/onboarding?next=http%3A%2F%2Flearn.mit.edu",
96+
})
7297
}
7398

7499
// this function sets up the test and progresses the UI to the designated step
@@ -107,8 +132,15 @@ describe("OnboardingPage", () => {
107132
const nextStep = step + 1
108133
await setupAndProgressToStep(step)
109134
if (step === STEP_TITLES.length - 1) {
110-
await findFinishButton()
135+
const finishButton = await findFinishButton()
136+
111137
expect(queryBackButton()).not.toBeNil()
138+
139+
await user.click(finishButton)
140+
await waitFor(() => {
141+
expect(window.location).toBe("http://learn.mit.edu")
142+
})
143+
112144
return
113145
}
114146

frontends/main/src/app-pages/OnboardingPage/OnboardingPage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { DASHBOARD_HOME } from "@/common/urls"
2727
import { useFormik } from "formik"
2828
import { useLearningResourceTopics } from "api/hooks/learningResources"
2929
import { useUserMe } from "api/hooks/user"
30+
31+
import { useSearchParams } from "next/navigation"
3032
import {
3133
CERTIFICATE_CHOICES,
3234
EDUCATION_LEVEL_OPTIONS,
@@ -156,6 +158,8 @@ const OnboardingPage: React.FC = () => {
156158
const { isLoading: userLoading, data: user } = useUserMe()
157159
const [activeStep, setActiveStep] = React.useState<number>(0)
158160
const router = useRouter()
161+
const searchParams = useSearchParams()
162+
const nextUrl = searchParams.get("next")
159163

160164
const formik = useFormik({
161165
enableReinitialize: true,
@@ -171,6 +175,10 @@ const OnboardingPage: React.FC = () => {
171175
if (activeStep < NUM_STEPS - 1) {
172176
setActiveStep((prevActiveStep) => prevActiveStep + 1)
173177
} else {
178+
if (nextUrl) {
179+
;(window as Window).location = nextUrl
180+
return null
181+
}
174182
router.push(DASHBOARD_HOME)
175183
}
176184
},

main/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@
196196
MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",)
197197

198198
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
199-
199+
MITOL_NEW_USER_LOGIN_URL = get_string(
200+
"MITOL_NEW_USER_LOGIN_URL", "http://open.odl.local:8062/onboarding"
201+
)
200202
LOGIN_REDIRECT_URL = "/app"
201203
LOGIN_URL = "/login"
202204
LOGIN_ERROR_URL = "/login"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.20 on 2025-04-15 14:14
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("profiles", "0034_remove_profile_scim_fields"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="profile",
14+
name="completed_onboarding",
15+
field=models.BooleanField(default=False),
16+
),
17+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 4.2.20 on 2025-04-07 19:05
2+
3+
from django.db import migrations
4+
5+
6+
def set_completed_onboarding(apps, schema_editor):
7+
Profile = apps.get_model("profiles", "Profile")
8+
Profile.objects.all().update(completed_onboarding=True)
9+
10+
11+
class Migration(migrations.Migration):
12+
dependencies = [
13+
("profiles", "0035_profile_completed_onboarding"),
14+
]
15+
16+
operations = [migrations.RunPython(set_completed_onboarding)]

profiles/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ class LearningResourceDelivery(models.TextChoices):
148148
email_optin = models.BooleanField(null=True)
149149
toc_optin = models.BooleanField(null=True)
150150

151+
completed_onboarding = models.BooleanField(default=False)
152+
151153
headline = models.CharField(blank=True, null=True, max_length=60) # noqa: DJ001
152154
bio = models.TextField(blank=True, null=True) # noqa: DJ001
153155
location = JSONField(null=True, blank=True)

0 commit comments

Comments
 (0)