diff --git a/RELEASE.rst b/RELEASE.rst index 3eb0c8b68a..46d72f09b1 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,13 @@ Release Notes ============= +Version 0.63.4 +-------------- + +- hide stay updated button on free products (#3201) +- Deindex unpublished test_mode resources, not contentfiles (#3192) +- feat: enable course access to staff user before the start date (#3182) + Version 0.63.3 (Released April 15, 2026) -------------- diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx index e64ca1167c..25a73fcc08 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx @@ -74,9 +74,13 @@ const futureDashboardCourse: typeof mitxOnlineCourse = (...overrides) => { const mitxUser = mitxonline.factories.user.user -const setupUserApis = () => { - const mitxUser = mitxonline.factories.user.user() - setMockResponse.get(mitxonline.urls.userMe.get(), mitxUser) +const setupUserApis = (overrides?: Parameters[0]) => { + const userData = mitxonline.factories.user.user({ + is_staff: false, + ...overrides, + }) + setMockResponse.get(mitxonline.urls.userMe.get(), userData) + return userData } describe.each([ @@ -246,6 +250,36 @@ describe.each([ }, ) + test("Courseware CTA is a navigable link for staff even when course has not started", async () => { + setupUserApis({ is_staff: true }) + const coursewareUrl = faker.internet.url() + const course = futureDashboardCourse() + const enrollment = mitxonline.factories.enrollment.courseEnrollment({ + enrollment_mode: EnrollmentMode.Audit, + grades: [], + certificate: null, + run: { + ...course.courseruns[0], + course: course, + courseware_url: coursewareUrl, + }, + }) + renderWithProviders( + , + ) + const card = getCard() + const coursewareCTA = await within(card).findByRole("link", { + name: "Continue", + }) + expect(coursewareCTA).toBeEnabled() + expect(coursewareCTA).toHaveAttribute("href", coursewareUrl) + }) + test("Courseware CTA is disabled when no enrollable runs exist", () => { setupUserApis() const course = mitxOnlineCourse({ diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx index 5a6a8a1c22..84ca2d3d0b 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx @@ -389,6 +389,7 @@ const useEnrollmentHandler = () => { createEnrollment.isPending || createVerifiedProgramEnrollment.isPending || replaceBasketItem.isPending, + mitxOnlineUser: mitxOnlineUser.data, } } @@ -402,6 +403,7 @@ type CoursewareButtonProps = { noun: string isProgram?: boolean isPending?: boolean + isStaff?: boolean "data-testid"?: string onClick?: React.MouseEventHandler } @@ -445,6 +447,7 @@ const CoursewareButton = styled( isProgram, onClick, isPending, + isStaff, ...others }: CoursewareButtonProps) => { const coursewareText = getCoursewareTextAndIcon({ @@ -457,7 +460,12 @@ const CoursewareButton = styled( const hasEnrolled = enrollmentStatus !== EnrollmentStatus.NotEnrolled // Programs or enrolled courses with started runs: show link - if ((isProgram || hasEnrolled) && (hasStarted || !startDate) && href) { + // Staff can access courseware even before the course has started + if ( + (isProgram || hasEnrolled) && + (hasStarted || !startDate || isStaff) && + href + ) { return ( = ({ onUpgradeError, }) => { const enrollment = useEnrollmentHandler() + const mitxOnlineUser = enrollment.mitxOnlineUser const useProductPages = useFeatureFlagEnabled( FeatureFlags.MitxOnlineProductPages, ) @@ -835,6 +846,7 @@ const DashboardCard: React.FC = ({ isProgram={false} disabled={disableEnrollment} isPending={enrollment.isPending} + isStaff={mitxOnlineUser?.is_staff} onClick={coursewareButtonClick} /> diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx index 0764c0221c..58018e2b9d 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx @@ -366,6 +366,7 @@ const useEnrollmentHandler = () => { createEnrollment.isPending || createVerifiedProgramEnrollment.isPending || replaceBasketItem.isPending, + mitxOnlineUser: mitxOnlineUser.data, } } @@ -376,6 +377,7 @@ type CoursewareButtonProps = { href?: string | null disabled?: boolean className?: string + isStaff?: boolean "data-testid"?: string onClick?: React.MouseEventHandler } @@ -413,6 +415,7 @@ const CoursewareButton = styled( disabled, className, onClick, + isStaff, ...others }: CoursewareButtonProps) => { const coursewareText = getCoursewareButtonStyle({ @@ -422,7 +425,8 @@ const CoursewareButton = styled( const hasStarted = startDate && isInPast(startDate) const hasEnrolled = enrollmentStatus !== EnrollmentStatus.NotEnrolled - if (hasEnrolled && (hasStarted || !startDate) && href) { + // Staff can access courseware even before the course has started + if (hasEnrolled && (hasStarted || !startDate || isStaff) && href) { return ( = ({ onUpgradeError, }) => { const enrollment = useEnrollmentHandler() + const mitxOnlineUser = enrollment.mitxOnlineUser const title = getTitle(resource) const enrollmentStatus = getDashboardEnrollmentStatus(resource) @@ -838,6 +845,7 @@ const DashboardCourseCard: React.FC = ({ href={buttonHref ?? coursewareUrl} endDate={courseRun?.end_date ?? enrollmentRun?.end_date} disabled={disableEnrollment} + isStaff={mitxOnlineUser?.is_staff} onClick={coursewareButtonClick} /> diff --git a/frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx b/frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx index 70ed43908d..3a0e876afa 100644 --- a/frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx @@ -13,6 +13,7 @@ import { renderWithProviders, waitFor, screen, within } from "@/test-utils" import CoursePage from "./CoursePage" import { assertHeadings } from "ol-test-utilities" import { notFound } from "next/navigation" +import { useStayUpdatedEnv } from "./test-utils/stayUpdated" import { useFeatureFlagEnabled } from "posthog-js/react" import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded" @@ -267,4 +268,96 @@ describe("CoursePage", () => { expect(notFound).toHaveBeenCalled() }) }) + + describe("Stay Updated button", () => { + useStayUpdatedEnv() + + test("Shows button when all course runs have only the verified enrollment mode", async () => { + const verifiedMode = factories.courses.enrollmentMode({ + mode_slug: "verified", + }) + const course = makeCourse({ + courseruns: [ + factories.courses.courseRun({ enrollment_modes: [verifiedMode] }), + factories.courses.courseRun({ enrollment_modes: [verifiedMode] }), + ], + }) + const page = makePage({ course_details: course }) + setupApis({ course, page }) + renderWithProviders() + + expect( + await screen.findByRole("button", { name: "Stay Updated" }), + ).toBeInTheDocument() + }) + + test.each([ + { + label: "one run has a non-verified mode", + buildRuns: () => [ + factories.courses.courseRun({ + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + ], + }), + factories.courses.courseRun({ + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "audit" }), + ], + }), + ], + }, + { + label: "a run has mixed verified and non-verified modes", + buildRuns: () => [ + factories.courses.courseRun({ + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + factories.courses.enrollmentMode({ mode_slug: "audit" }), + ], + }), + ], + }, + { + label: "a run has no enrollment modes", + buildRuns: () => [ + factories.courses.courseRun({ enrollment_modes: [] }), + ], + }, + { + label: "the course has no runs", + buildRuns: () => [], + }, + ])("Hides button when $label", async ({ buildRuns }) => { + const course = makeCourse({ courseruns: buildRuns() }) + const page = makePage({ course_details: course }) + setupApis({ course, page }) + renderWithProviders() + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }) + + test("Hides button when Stay Updated form ID is not configured", async () => { + delete process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID + const verifiedMode = factories.courses.enrollmentMode({ + mode_slug: "verified", + }) + const course = makeCourse({ + courseruns: [ + factories.courses.courseRun({ enrollment_modes: [verifiedMode] }), + ], + }) + const page = makePage({ course_details: course }) + setupApis({ course, page }) + renderWithProviders() + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }) + }) }) diff --git a/frontends/main/src/app-pages/ProductPages/CoursePage.tsx b/frontends/main/src/app-pages/ProductPages/CoursePage.tsx index 15cf6d7f1d..b351d5c630 100644 --- a/frontends/main/src/app-pages/ProductPages/CoursePage.tsx +++ b/frontends/main/src/app-pages/ProductPages/CoursePage.tsx @@ -18,6 +18,7 @@ import ProductPageTemplate from "./ProductPageTemplate" import WhatYoullLearnSection from "./WhatYoullLearnSection" import HowYoullLearnSection, { DEFAULT_HOW_DATA } from "./HowYoullLearnSection" import { DEFAULT_RESOURCE_IMG } from "ol-utilities" +import { isVerifiedEnrollmentMode } from "@/common/mitxonline" import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded" import CourseInfoBox from "./InfoBoxCourse" import CourseEnrollmentButton from "./CourseEnrollmentButton" @@ -76,6 +77,16 @@ const CoursePage: React.FC = ({ readableId }) => { enrollmentAction={ } + showStayUpdated={ + course.courseruns.length > 0 && + course.courseruns.every( + (run) => + run.enrollment_modes.length > 0 && + run.enrollment_modes.every((mode) => + isVerifiedEnrollmentMode(mode.mode_slug), + ), + ) + } > {page.about ? ( diff --git a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.test.tsx b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.test.tsx index 3a8a393f85..65ba6d6986 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.test.tsx @@ -4,6 +4,7 @@ import { renderWithProviders, screen } from "@/test-utils" import ProductPageTemplate from "./ProductPageTemplate" import { useHubspotFormDetail } from "api/hooks/hubspot" import NiceModal from "@ebay/nice-modal-react" +import { STAY_UPDATED_FORM_ID } from "./test-utils/stayUpdated" jest.mock("api/hooks/hubspot", () => ({ ...jest.requireActual("api/hooks/hubspot"), @@ -27,9 +28,11 @@ const mockedNiceModalShow = NiceModal.show as jest.MockedFunction< typeof NiceModal.show > -const STAY_UPDATED_FORM_ID = "4f423dc7-5b08-430b-a9fb-920b7f9597ed" - -const renderProductPageTemplate = () => { +const renderProductPageTemplate = ({ + showStayUpdated, +}: { + showStayUpdated?: boolean +} = {}) => { setMockResponse.get(urls.userMe.get(), { is_authenticated: false }) renderWithProviders( { imageSrc="/test-image.jpg" infoBox={
Info box
} enrollmentAction={} + showStayUpdated={showStayUpdated} >
Page content
, @@ -66,7 +70,9 @@ describe("ProductPageTemplate stay-updated trigger", () => { expect( screen.queryByRole("button", { name: "Stay Updated" }), ).not.toBeInTheDocument() - expect(mockedUseHubspotFormDetail).toHaveBeenCalledWith(undefined) + expect(mockedUseHubspotFormDetail).toHaveBeenCalledWith(undefined, { + enabled: false, + }) }) it("opens the modal when form id is configured even if the form is not yet fetched", () => { @@ -76,7 +82,7 @@ describe("ProductPageTemplate stay-updated trigger", () => { isError: false, } as ReturnType) - renderProductPageTemplate() + renderProductPageTemplate({ showStayUpdated: true }) const button = screen.getByRole("button", { name: "Stay Updated" }) expect(button).toBeInTheDocument() @@ -93,7 +99,7 @@ describe("ProductPageTemplate stay-updated trigger", () => { isError: true, } as ReturnType) - renderProductPageTemplate() + renderProductPageTemplate({ showStayUpdated: true }) const button = screen.getByRole("button", { name: "Stay Updated" }) expect(button).toBeInTheDocument() @@ -110,7 +116,7 @@ describe("ProductPageTemplate stay-updated trigger", () => { isError: false, } as unknown as ReturnType) - renderProductPageTemplate() + renderProductPageTemplate({ showStayUpdated: true }) const button = screen.getByRole("button", { name: "Stay Updated" }) expect(button).toBeInTheDocument() diff --git a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx index da7001f22b..476750e8c9 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx @@ -263,6 +263,7 @@ type ProductPageTemplateProps = { infoBox: React.ReactNode enrollmentAction: React.ReactNode children: React.ReactNode + showStayUpdated?: boolean } const ProductPageTemplate: React.FC = ({ currentBreadcrumbLabel, @@ -273,13 +274,18 @@ const ProductPageTemplate: React.FC = ({ infoBox, children, enrollmentAction, + showStayUpdated, }) => { const stayUpdatedFormId = getStayUpdatedHubspotFormId() + const shouldShowStayUpdatedButton = Boolean( + stayUpdatedFormId && showStayUpdated, + ) const stayUpdatedParams = stayUpdatedFormId ? { form_id: stayUpdatedFormId } : undefined - const formQuery = useHubspotFormDetail(stayUpdatedParams) - const shouldShowStayUpdatedButton = Boolean(stayUpdatedFormId) + const formQuery = useHubspotFormDetail(stayUpdatedParams, { + enabled: shouldShowStayUpdatedButton, + }) return ( diff --git a/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx index 3ea3484713..188754fba3 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx @@ -20,6 +20,10 @@ import { assertHeadings } from "ol-test-utilities" import ProgramAsCoursePage from "./ProgramAsCoursePage" import { notFound } from "next/navigation" import { useFeatureFlagEnabled } from "posthog-js/react" +import { + useStayUpdatedEnv, + PROGRAM_HIDE_STAY_UPDATED_CASES, +} from "./test-utils/stayUpdated" import invariant from "tiny-invariant" import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded" import { getIdsFromReqTree } from "@/common/mitxonline" @@ -294,4 +298,63 @@ describe("ProgramAsCoursePage", () => { expect(listItems[1]).toHaveTextContent(childPrograms[0].title) expect(listItems[2]).toHaveTextContent(courses[1].title) }) + + describe("Stay Updated button", () => { + useStayUpdatedEnv() + + test("Shows button when program has only the verified enrollment mode", async () => { + const program = makeProgramAsCourse({ + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + ], + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders( + , + ) + + expect( + await screen.findByRole("button", { name: "Stay Updated" }), + ).toBeInTheDocument() + }) + + test.each(PROGRAM_HIDE_STAY_UPDATED_CASES)( + "Hides button when $label", + async ({ enrollment_modes: enrollmentModes }) => { + const program = makeProgramAsCourse({ + enrollment_modes: enrollmentModes, + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders( + , + ) + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }, + ) + + test("Hides button when Stay Updated form ID is not configured", async () => { + delete process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID + const program = makeProgramAsCourse({ + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + ], + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders( + , + ) + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }) + }) }) diff --git a/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx b/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx index 155c54da58..d51e9c9ff6 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx @@ -11,7 +11,10 @@ import { FeatureFlags } from "@/common/feature_flags" import { notFound } from "next/navigation" import { HeadingIds, parseReqTree } from "./util" import type { RequirementItem } from "./util" -import { getIdsFromReqTree } from "@/common/mitxonline" +import { + getIdsFromReqTree, + isVerifiedEnrollmentMode, +} from "@/common/mitxonline" import InstructorsSection from "./InstructorsSection" import RawHTML from "./RawHTML" import UnstyledRawHTML from "@/components/UnstyledRawHTML/UnstyledRawHTML" @@ -280,6 +283,12 @@ const ProgramAsCoursePage: React.FC = ({ displayAsCourse /> } + showStayUpdated={ + program.enrollment_modes.length > 0 && + program.enrollment_modes.every((mode) => + isVerifiedEnrollmentMode(mode.mode_slug), + ) + } infoBox={ { expect(notFound).toHaveBeenCalled() }) }) + + describe("Stay Updated button", () => { + useStayUpdatedEnv() + + test("Shows button when program has only the verified enrollment mode", async () => { + const program = makeProgram({ + ...makeReqs(), + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + ], + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders() + + expect( + await screen.findByRole("button", { name: "Stay Updated" }), + ).toBeInTheDocument() + }) + + test.each(PROGRAM_HIDE_STAY_UPDATED_CASES)( + "Hides button when $label", + async ({ enrollment_modes: enrollmentModes }) => { + const program = makeProgram({ + ...makeReqs(), + enrollment_modes: enrollmentModes, + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders() + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }, + ) + + test("Hides button when Stay Updated form ID is not configured", async () => { + delete process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID + const program = makeProgram({ + ...makeReqs(), + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + ], + }) + const page = makePage({ program_details: program }) + setupApis({ program, page }) + renderWithProviders() + + await screen.findByRole("heading", { name: page.title }) + expect( + screen.queryByRole("button", { name: "Stay Updated" }), + ).not.toBeInTheDocument() + }) + }) }) diff --git a/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx b/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx index 01042acff8..e17659d664 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx @@ -11,7 +11,10 @@ import { useFeatureFlagEnabled } from "posthog-js/react" import { FeatureFlags } from "@/common/feature_flags" import { notFound } from "next/navigation" import { HeadingIds, parseReqTree, RequirementData } from "./util" -import { getIdsFromReqTree } from "@/common/mitxonline" +import { + getIdsFromReqTree, + isVerifiedEnrollmentMode, +} from "@/common/mitxonline" import InstructorsSection from "./InstructorsSection" import RawHTML from "./RawHTML" import UnstyledRawHTML from "@/components/UnstyledRawHTML/UnstyledRawHTML" @@ -279,6 +282,12 @@ const ProgramPage: React.FC = ({ readableId }) => { enrollmentAction={ } + showStayUpdated={ + program.enrollment_modes.length > 0 && + program.enrollment_modes.every((mode) => + isVerifiedEnrollmentMode(mode.mode_slug), + ) + } infoBox={ } diff --git a/frontends/main/src/app-pages/ProductPages/StayUpdatedModal.test.tsx b/frontends/main/src/app-pages/ProductPages/StayUpdatedModal.test.tsx index c2ba27ceaa..c912c9fd1b 100644 --- a/frontends/main/src/app-pages/ProductPages/StayUpdatedModal.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/StayUpdatedModal.test.tsx @@ -4,6 +4,7 @@ import { HubspotForm, type HubspotFormProps } from "ol-components" import { setMockResponse, urls, factories } from "api/test-utils" import { renderWithProviders, screen, user, act } from "@/test-utils" import { StayUpdatedModal } from "./StayUpdatedModal" +import { STAY_UPDATED_FORM_ID } from "./test-utils/stayUpdated" jest.mock("ol-components", () => ({ ...jest.requireActual("ol-components"), @@ -11,8 +12,6 @@ jest.mock("ol-components", () => ({ })) const mockedHubspotForm = jest.mocked(HubspotForm) - -const STAY_UPDATED_FORM_ID = "4f423dc7-5b08-430b-a9fb-920b7f9597ed" const TEST_EMAIL = "user@test.edu" const setupApis = () => { diff --git a/frontends/main/src/app-pages/ProductPages/test-utils/stayUpdated.ts b/frontends/main/src/app-pages/ProductPages/test-utils/stayUpdated.ts new file mode 100644 index 0000000000..e070b06152 --- /dev/null +++ b/frontends/main/src/app-pages/ProductPages/test-utils/stayUpdated.ts @@ -0,0 +1,53 @@ +import { factories } from "api/mitxonline-test-utils" +import type { EnrollmentMode } from "@mitodl/mitxonline-api-axios/v2" +import { faker } from "@faker-js/faker/locale/en" + +export const STAY_UPDATED_FORM_ID = faker.string.uuid() + +/** + * Sets the Stay Updated Hubspot form ID env var before each test and removes + * it after. Call inside a describe block. + */ +export const useStayUpdatedEnv = () => { + let previousFormId: string | undefined + + beforeEach(() => { + previousFormId = process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID + process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID = STAY_UPDATED_FORM_ID + }) + + afterEach(() => { + if (previousFormId === undefined) { + delete process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID + } else { + process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID = previousFormId + } + }) +} + +/** + * Shared test.each cases for the "Stay Updated button is hidden" scenarios on + * program-level enrollment_modes (used by ProgramPage and ProgramAsCoursePage). + */ +export const PROGRAM_HIDE_STAY_UPDATED_CASES: { + label: string + enrollment_modes: EnrollmentMode[] +}[] = [ + { + label: "program has a non-verified enrollment mode", + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "audit" }), + ], + }, + { + label: "program has mixed verified and non-verified modes", + enrollment_modes: [ + factories.courses.enrollmentMode({ mode_slug: "verified" }), + factories.courses.enrollmentMode({ mode_slug: "audit" }), + ], + }, + { + label: "program has no enrollment modes", + enrollment_modes: [], + }, +] diff --git a/learning_resources/etl/loaders.py b/learning_resources/etl/loaders.py index a64eb7c92e..d1c6fe01a5 100644 --- a/learning_resources/etl/loaders.py +++ b/learning_resources/etl/loaders.py @@ -80,11 +80,7 @@ def update_index(learning_resource, newly_created): learning resource (LearningResource): a learning resource newly_created (bool): whether the learning resource has just been created """ - if ( - not newly_created - and not learning_resource.published - and not learning_resource.test_mode - ): + if not newly_created and not learning_resource.published: resource_unpublished_actions(learning_resource) elif learning_resource.published: resource_upserted_actions( diff --git a/learning_resources/etl/loaders_test.py b/learning_resources/etl/loaders_test.py index bdcd11ea55..8649bd7c25 100644 --- a/learning_resources/etl/loaders_test.py +++ b/learning_resources/etl/loaders_test.py @@ -185,17 +185,17 @@ def test_update_index_test_mode_behavior( test_mode, newly_created, ): - """Test update_index does not remove test_mode content files from index""" + """Test update_index unpublishes existing unpublished resources""" resource_unpublished_actions = mocker.patch( "learning_resources.etl.loaders.resource_unpublished_actions" ) lr = LearningResourceFactory.create(published=published, test_mode=test_mode) loaders.update_index(lr, newly_created) - if test_mode: - resource_unpublished_actions.assert_not_called() - elif not published and not newly_created: + if not published and not newly_created: resource_unpublished_actions.assert_called_once() + else: + resource_unpublished_actions.assert_not_called() @pytest.fixture diff --git a/learning_resources_search/plugins.py b/learning_resources_search/plugins.py index 1882622475..34e245592c 100644 --- a/learning_resources_search/plugins.py +++ b/learning_resources_search/plugins.py @@ -104,7 +104,7 @@ def resource_unpublished(self, resource): ) try_with_retry_as_task(chain(*unpublished_tasks)) - if resource.resource_type == COURSE_TYPE: + if resource.resource_type == COURSE_TYPE and not resource.test_mode: for run in resource.runs.all(): self.resource_run_unpublished(run) @@ -159,6 +159,8 @@ def resource_before_delete(self, resource): """ Remove a resource from the search index and then delete the object """ + # Ensure test mode is false so the resource is removed from the search index + resource.test_mode = False self.resource_unpublished(resource) @hookimpl diff --git a/learning_resources_search/plugins_test.py b/learning_resources_search/plugins_test.py index b4ff44c541..517614a596 100644 --- a/learning_resources_search/plugins_test.py +++ b/learning_resources_search/plugins_test.py @@ -89,11 +89,14 @@ def test_search_index_plugin_resource_upserted( @pytest.mark.django_db @pytest.mark.parametrize("resource_type", [COURSE_TYPE, PROGRAM_TYPE]) @pytest.mark.parametrize("has_content_files", [True, False]) +@pytest.mark.parametrize("test_mode", [True, False]) def test_search_index_plugin_resource_unpublished( - mocker, mock_search_index_helpers, resource_type, has_content_files + mocker, mock_search_index_helpers, resource_type, has_content_files, test_mode ): """The plugin function should remove a resource from the search index""" - resource = LearningResourceFactory.create(resource_type=resource_type) + resource = LearningResourceFactory.create( + resource_type=resource_type, test_mode=test_mode + ) if resource_type == COURSE_TYPE and has_content_files: for run in resource.runs.all(): ContentFileFactory.create(run=run) @@ -104,7 +107,7 @@ def test_search_index_plugin_resource_unpublished( mock_search_index_helpers.mock_remove_learning_resource_immutable_signature.assert_called_once_with( resource.id, resource.resource_type ) - if resource_type == COURSE_TYPE and has_content_files: + if resource_type == COURSE_TYPE and has_content_files and not test_mode: assert unpublish_run_mock.call_count == resource.runs.count() for run in resource.runs.all(): unpublish_run_mock.assert_any_call(run.id, unpublished_only=False) @@ -114,17 +117,39 @@ def test_search_index_plugin_resource_unpublished( @pytest.mark.django_db @pytest.mark.parametrize("resource_type", [COURSE_TYPE, PROGRAM_TYPE]) +@pytest.mark.parametrize("test_mode", [True, False]) def test_search_index_plugin_resource_before_delete( - mock_search_index_helpers, resource_type + mock_search_index_helpers, resource_type, test_mode ): """The plugin function should remove a resource from the search index then delete the resource""" - resource = LearningResourceFactory.create(resource_type=resource_type) + resource = LearningResourceFactory.create( + resource_type=resource_type, + test_mode=test_mode, + ) + if resource_type == COURSE_TYPE: + for run in resource.runs.all(): + ContentFileFactory.create(run=run) + resource_id = resource.id SearchIndexPlugin().resource_before_delete(resource) mock_search_index_helpers.mock_remove_learning_resource_immutable_signature.assert_called_once_with( resource_id, resource.resource_type ) + assert resource.test_mode is False + + if resource_type == COURSE_TYPE: + assert ( + mock_search_index_helpers.mock_remove_contentfiles_immutable_signature.call_count + == resource.runs.count() + ) + for run in resource.runs.all(): + mock_search_index_helpers.mock_remove_contentfiles_immutable_signature.assert_any_call( + run.id, + unpublished_only=False, + ) + else: + mock_search_index_helpers.mock_remove_contentfiles_immutable_signature.assert_not_called() @pytest.mark.django_db diff --git a/main/settings.py b/main/settings.py index f658f33bac..5843b656e3 100644 --- a/main/settings.py +++ b/main/settings.py @@ -34,7 +34,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.63.3" +VERSION = "0.63.4" log = logging.getLogger()