diff --git a/RELEASE.rst b/RELEASE.rst index 5015429c55..847de3fa9d 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,17 @@ Release Notes ============= +Version 0.13.9 +-------------- + +- Updates to page titles (#1121) +- Shanbady/minor UI updates (#1118) +- Shanbady/navigation UI fixes (#1119) +- mitx - only ingest published courses (#1102) +- Make resource.prices = most recent published run prices if there is no next run (#1116) +- switch default sort to use popular instead of created on (#1120) +- Fix populate_featured_lists mgmt command (#1097) + Version 0.13.8 (Released June 20, 2024) -------------- diff --git a/frontends/jest.jsdom.config.ts b/frontends/jest.jsdom.config.ts index 6c7ce2b08f..246e6514bd 100644 --- a/frontends/jest.jsdom.config.ts +++ b/frontends/jest.jsdom.config.ts @@ -1,6 +1,8 @@ import { resolve } from "path" import type { Config } from "@jest/types" +process.env.SITE_NAME = "MIT Open" + /** * Base configuration for jest tests. */ diff --git a/frontends/mit-open/src/page-components/Footer/Footer.test.tsx b/frontends/mit-open/src/page-components/Footer/Footer.test.tsx index fd0d6dff2e..09f2b25324 100644 --- a/frontends/mit-open/src/page-components/Footer/Footer.test.tsx +++ b/frontends/mit-open/src/page-components/Footer/Footer.test.tsx @@ -19,7 +19,6 @@ describe("Footer", () => { "About Us": urls.ABOUT, Accessibility: urls.ACCESSIBILITY, "Privacy Policy": urls.PRIVACY, - "Terms of Service": urls.TERMS, "Contact Us": urls.CONTACT, } const footer = screen.getByRole("contentinfo") @@ -35,7 +34,7 @@ describe("Footer", () => { expect(address).toHaveTextContent("Massachusetts Institute of Technology") expect(address).toHaveTextContent("77 Massachusetts Avenue") expect(address).toHaveTextContent("Cambridge, MA 02139") - expect(links).toHaveLength(7) + expect(links).toHaveLength(6) for (const link of links) { expect(link).toHaveAttribute( "href", diff --git a/frontends/mit-open/src/page-components/Footer/Footer.tsx b/frontends/mit-open/src/page-components/Footer/Footer.tsx index beb7b40f3d..2de4695d0e 100644 --- a/frontends/mit-open/src/page-components/Footer/Footer.tsx +++ b/frontends/mit-open/src/page-components/Footer/Footer.tsx @@ -171,10 +171,6 @@ const Footer: FunctionComponent = () => { text="Privacy Policy" href={urls.PRIVACY} /> - diff --git a/frontends/mit-open/src/page-components/Header/Header.test.tsx b/frontends/mit-open/src/page-components/Header/Header.test.tsx index 8d689c7619..39c6962901 100644 --- a/frontends/mit-open/src/page-components/Header/Header.test.tsx +++ b/frontends/mit-open/src/page-components/Header/Header.test.tsx @@ -98,7 +98,7 @@ describe("UserMenu", () => { test("Authenticated users see the Log Out link", async () => { const isAuthenticated = true const initialUrl = "/foo/bar?cat=meow" - const expected = { text: "Log out", url: urlConstants.LOGOUT } + const expected = { text: "Log Out", url: urlConstants.LOGOUT } setMockResponse.get(urls.userMe.get(), { is_authenticated: isAuthenticated, }) diff --git a/frontends/mit-open/src/page-components/Header/Header.tsx b/frontends/mit-open/src/page-components/Header/Header.tsx index 5814d256da..c4e32ff477 100644 --- a/frontends/mit-open/src/page-components/Header/Header.tsx +++ b/frontends/mit-open/src/page-components/Header/Header.tsx @@ -27,6 +27,7 @@ import { useUserMe } from "api/hooks/user" const Bar = styled(AppBar)(({ theme }) => ({ height: "60px", padding: "0 8px", + borderBottom: `1px solid ${theme.custom.colors.lightGray2}`, backgroundColor: theme.custom.colors.white, color: theme.custom.colors.darkGray1, display: "flex", diff --git a/frontends/mit-open/src/page-components/Header/UserMenu.tsx b/frontends/mit-open/src/page-components/Header/UserMenu.tsx index 2d062b395c..f06c30bfa8 100644 --- a/frontends/mit-open/src/page-components/Header/UserMenu.tsx +++ b/frontends/mit-open/src/page-components/Header/UserMenu.tsx @@ -27,7 +27,7 @@ const UserMenuContainer = styled.button({ }) const LoginButtonContainer = styled(FlexContainer)(({ theme }) => ({ - paddingRight: "32px", + paddingRight: "16px", "&:hover": { textDecoration: "none", }, @@ -109,12 +109,6 @@ const UserMenu: React.FC = ({ variant }) => { allow: !!user?.is_authenticated, href: urls.DASHBOARD, }, - { - label: "User Lists", - key: "userlists", - allow: !!user?.is_authenticated, - href: urls.USERLIST_LISTING, - }, { label: "Learning Paths", key: "learningpaths", @@ -122,7 +116,7 @@ const UserMenu: React.FC = ({ variant }) => { href: urls.LEARNINGPATH_LISTING, }, { - label: "Log out", + label: "Log Out", key: "logout", allow: !!user?.is_authenticated, href: urls.LOGOUT, @@ -159,12 +153,11 @@ const UserMenu: React.FC = ({ variant }) => { - Sign Up / Login + Sign Up / Log In ) : ( diff --git a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx index 6bae4eecd4..ef46c83708 100644 --- a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx +++ b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx @@ -38,7 +38,7 @@ const SearchSubscriptionToggle = ({ return [ { key: "unsubscribe", - label: "Unsubscribe", + label: "Unfollow", onClick: () => unsubscribe(subscriptionId), }, ] @@ -51,7 +51,7 @@ const SearchSubscriptionToggle = ({ }> - Subscribed + Follow } items={unsubscribeItems} @@ -69,7 +69,7 @@ const SearchSubscriptionToggle = ({ }) } > - Subscribe + Follow ) } diff --git a/frontends/mit-open/src/pages/AboutPage/AboutPage.test.tsx b/frontends/mit-open/src/pages/AboutPage/AboutPage.test.tsx index 8aada28166..07c2d1f9f6 100644 --- a/frontends/mit-open/src/pages/AboutPage/AboutPage.test.tsx +++ b/frontends/mit-open/src/pages/AboutPage/AboutPage.test.tsx @@ -18,7 +18,7 @@ describe("AboutPage", () => { url: commonUrls.ABOUT, }) await waitFor(() => { - expect(document.title).toBe("About Us") + expect(document.title).toBe("About Us | MIT Open") }) screen.getByRole("heading", { name: "About Us", diff --git a/frontends/mit-open/src/pages/AboutPage/AboutPage.tsx b/frontends/mit-open/src/pages/AboutPage/AboutPage.tsx index 7eadf4aac2..402ef18df5 100644 --- a/frontends/mit-open/src/pages/AboutPage/AboutPage.tsx +++ b/frontends/mit-open/src/pages/AboutPage/AboutPage.tsx @@ -79,9 +79,7 @@ const AboutPage: React.FC = () => { return ( - - About Us - + { screen.getByText(article.html) await waitFor(() => { - expect(document.title).toBe(article.title) + expect(document.title).toBe(`${article.title} | MIT Open`) }) }) diff --git a/frontends/mit-open/src/pages/ArticleDetailsPage/ArticleDetailsPage.tsx b/frontends/mit-open/src/pages/ArticleDetailsPage/ArticleDetailsPage.tsx index fc4d5d4839..7c6fde5f07 100644 --- a/frontends/mit-open/src/pages/ArticleDetailsPage/ArticleDetailsPage.tsx +++ b/frontends/mit-open/src/pages/ArticleDetailsPage/ArticleDetailsPage.tsx @@ -26,9 +26,7 @@ const ArticlesDetailPage: React.FC = () => { src="/static/images/course_search_banner.png" className="articles-detail-page" > - - {article.data?.title} - + diff --git a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx index 127a358f7a..f122151b12 100644 --- a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx +++ b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx @@ -41,7 +41,7 @@ describe("ArticleEditPage", () => { expect(bodyInput).toBeInstanceOf(HTMLTextAreaElement) await waitFor(() => { - expect(document.title).toBe(`Editing: ${article.title}`) + expect(document.title).toBe(`${article.title} | Edit | MIT Open`) }) }) diff --git a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.tsx b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.tsx index 708f39ef22..01a7920536 100644 --- a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.tsx +++ b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.tsx @@ -21,7 +21,7 @@ const ArticleEditPage: React.FC = () => { [navigate, id], ) const goHome = useCallback(() => navigate("/"), [navigate]) - const title = `Editing: ${article.data?.title ?? ""}` + const title = article.data?.title ? `${article.data?.title} | Edit` : "Edit" return ( = ({ }) => { return ( - - {title} - + {children} diff --git a/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx b/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx index d93e184c76..099bd581da 100644 --- a/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx +++ b/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx @@ -191,7 +191,7 @@ describe("DashboardPage", () => { setupAPIs() renderWithProviders() await waitFor(() => { - expect(document.title).toBe("User Home") + expect(document.title).toBe("Your MIT Learning Journey | MIT Open") }) screen.getByRole("heading", { name: "Your MIT Learning Journey", diff --git a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx index d99dec8f2d..e7fdc00654 100644 --- a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx +++ b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx @@ -415,9 +415,7 @@ const DashboardPage: React.FC = () => { - - User Home - + diff --git a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx index 2140ed2289..7d67007d27 100644 --- a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx +++ b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx @@ -82,11 +82,11 @@ describe("DepartmentListingPage", () => { } } - it("Has a page title", async () => { + it("Has correct page title", async () => { setupApis() renderWithProviders() await waitFor(() => { - expect(document.title).toBe("MIT Open | Departments") + expect(document.title).toBe("Departments | MIT Open") }) screen.getByRole("heading", { name: "Departments" }) }) diff --git a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx index 0bb679af79..90a4beed94 100644 --- a/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx +++ b/frontends/mit-open/src/pages/DepartmentListingPage/DepartmentListingPage.tsx @@ -215,9 +215,7 @@ const DepartmentListingPage: React.FC = () => { return ( - - MIT Open | Departments - + { - expect(document.title).toBe("Not Allowed") + expect(document.title).toBe("Not Allowed | MIT Open") }) }, ) @@ -54,7 +54,7 @@ test("ErrorPage shows NotFound on API 404 errors", async () => { allowConsoleErrors() setup(404) await waitFor(() => { - expect(document.title).toBe("Not Found") + expect(document.title).toBe("Not Found | MIT Open") }) }) @@ -62,7 +62,7 @@ test("ErrorPage shows NotFound on frontend routing 404 errors", async () => { allowConsoleErrors() renderTestApp({ url: "/some-fake-route" }) await waitFor(() => { - expect(document.title).toBe("Not Found") + expect(document.title).toBe("Not Found | MIT Open") }) }) @@ -87,7 +87,7 @@ test("ErrorPage shows ForbiddenPage on restricted routes.", async () => { { user: { is_authenticated: true } }, ) await waitFor(() => { - expect(document.title).toBe("Not Allowed") + expect(document.title).toBe("Not Allowed | MIT Open") }) }) @@ -107,7 +107,7 @@ test("ErrorPage shows fallback and logs unexpected errors", async () => { { user: { is_authenticated: true } }, ) await waitFor(() => { - expect(document.title).toBe("Not Allowed") + expect(document.title).toBe("Not Allowed | MIT Open") }) expect(consoleError).toHaveBeenCalledWith(Error("Some Error")) }) diff --git a/frontends/mit-open/src/pages/ErrorPage/ErrorPageTemplate.tsx b/frontends/mit-open/src/pages/ErrorPage/ErrorPageTemplate.tsx index fa14bc1080..f3a889d8fd 100644 --- a/frontends/mit-open/src/pages/ErrorPage/ErrorPageTemplate.tsx +++ b/frontends/mit-open/src/pages/ErrorPage/ErrorPageTemplate.tsx @@ -21,9 +21,8 @@ const ErrorPageTemplate: React.FC = ({ return ( - + - {title} {children} diff --git a/frontends/mit-open/src/pages/ErrorPage/ForbiddenPage.test.tsx b/frontends/mit-open/src/pages/ErrorPage/ForbiddenPage.test.tsx index f5b428e779..decd9931f2 100644 --- a/frontends/mit-open/src/pages/ErrorPage/ForbiddenPage.test.tsx +++ b/frontends/mit-open/src/pages/ErrorPage/ForbiddenPage.test.tsx @@ -31,7 +31,7 @@ test("The ForbiddenPage loads with meta", async () => { }) renderWithProviders() await waitFor(() => { - expect(document.title).toBe("Not Allowed") + expect(document.title).toBe("Not Allowed | MIT Open") }) const meta = document.head.querySelector('meta[name="robots"]') diff --git a/frontends/mit-open/src/pages/FieldPage/EditFieldPage.tsx b/frontends/mit-open/src/pages/FieldPage/EditFieldPage.tsx index 52e0c0019b..f2cc45800b 100644 --- a/frontends/mit-open/src/pages/FieldPage/EditFieldPage.tsx +++ b/frontends/mit-open/src/pages/FieldPage/EditFieldPage.tsx @@ -38,9 +38,7 @@ const EditFieldPage: React.FC = () => { name={field.data?.name} channelType={field.data?.channel_type} > - - Edit {field.data.title} Channel - + {field.data.is_moderator ? (
diff --git a/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx b/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx index de419fb1d8..feda034f4f 100644 --- a/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx +++ b/frontends/mit-open/src/pages/FieldPage/FieldPage.test.tsx @@ -191,10 +191,10 @@ describe("FieldPage", () => { const { field } = setupApis({ search_filter: "q=ocw" }, {}, true, true) renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) await screen.findByText(field.title) - const subscribedButton = await screen.findByText("Subscribed") + const subscribedButton = await screen.findByText("Follow") assertInstanceOf(subscribedButton, HTMLButtonElement) user.click(subscribedButton) - const unsubscribeButton = await screen.findByText("Unsubscribe") + const unsubscribeButton = await screen.findByText("Unfollow") assertInstanceOf(unsubscribeButton, HTMLLIElement) }) @@ -202,7 +202,7 @@ describe("FieldPage", () => { const { field } = setupApis({ search_filter: "q=ocw" }, {}, true, false) renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) await screen.findByText(field.title) - const subscribeButton = await screen.findByText("Subscribe") + const subscribeButton = await screen.findByText("Follow") assertInstanceOf(subscribeButton, HTMLButtonElement) }) it("Hides the subscribe toggle if the user is not authenticated", async () => { @@ -210,7 +210,7 @@ describe("FieldPage", () => { renderTestApp({ url: `/c/${field.channel_type}/${field.name}` }) await screen.findByText(field.title) await waitFor(() => { - expect(screen.queryByText("Subscribe")).not.toBeInTheDocument() + expect(screen.queryByText("Follow")).not.toBeInTheDocument() }) }) }) diff --git a/frontends/mit-open/src/pages/FieldPage/FieldPageSkeleton.tsx b/frontends/mit-open/src/pages/FieldPage/FieldPageSkeleton.tsx index 4e08e79213..a113d85d33 100644 --- a/frontends/mit-open/src/pages/FieldPage/FieldPageSkeleton.tsx +++ b/frontends/mit-open/src/pages/FieldPage/FieldPageSkeleton.tsx @@ -9,6 +9,7 @@ import { Box, Breadcrumbs, } from "ol-components" +import { MetaTags } from "ol-utilities" import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle" import { ChannelDetails } from "@/page-components/ChannelDetails/ChannelDetails" import { useChannelDetail } from "api/hooks/fields" @@ -101,89 +102,110 @@ const FieldSkeletonProps: React.FC = ({ ] return ( - - - - {field.data && ( - + <> + + + + + {field.data && ( ({ + flexDirection="column" + alignItems="start" + sx={{ flexGrow: 1, flexShrink: 0, order: 1, - py: "24px", - - [theme.breakpoints.down("md")]: { - py: 0, - pb: "8px", - }, - })} + width: "50%", + }} > - {displayConfiguration?.logo ? ( - + ({ + flexGrow: 1, + flexShrink: 0, + order: 1, + py: "24px", + + [theme.breakpoints.down("md")]: { + py: 0, + pb: "8px", + }, + })} + > + {displayConfiguration?.logo ? ( + + ) : ( + + + {field.data.title} + + + )} + + {displayConfiguration.heading ? ( + + + {displayConfiguration.heading} + + ) : ( - - - {field.data.title} - - + <> )} - - {displayConfiguration.heading ? ( = ({ my: 1, }} > - - {displayConfiguration.heading} + + {displayConfiguration.sub_heading} - ) : ( - <> - )} - - - {displayConfiguration.sub_heading} - + {channelType === "unit" ? ( + + + {field.data?.search_filter ? ( + + ) : null} + {field.data?.is_moderator ? ( + + ) : null} + + + ) : null} {channelType === "unit" ? ( + + + + ) : ( @@ -248,69 +299,24 @@ const FieldSkeletonProps: React.FC = ({ ) : null} - ) : null} + )} - {channelType === "unit" ? ( - - - - ) : ( - - - {field.data?.search_filter ? ( - - ) : null} - {field.data?.is_moderator ? ( - - ) : null} - - - )} - - )} - - - } - > - {channelType === "unit" ? ( - - - - ) : null} - {children} - + )} + + + } + > + {channelType === "unit" ? ( + + + + ) : null} + {children} + + ) } diff --git a/frontends/mit-open/src/pages/HomePage/HomePage.tsx b/frontends/mit-open/src/pages/HomePage/HomePage.tsx index 8872ff9c03..bf90af1250 100644 --- a/frontends/mit-open/src/pages/HomePage/HomePage.tsx +++ b/frontends/mit-open/src/pages/HomePage/HomePage.tsx @@ -1,5 +1,6 @@ import React from "react" import { Container, styled } from "ol-components" +import { MetaTags } from "ol-utilities" import HeroSearch from "./HeroSearch" import BrowseTopicsSection from "./BrowseTopicsSection" import NewsEventsSection from "./NewsEventsSection" @@ -35,6 +36,7 @@ const MediaCarousel = styled(ResourceCarousel)(({ theme }) => ({ const HomePage: React.FC = () => { return ( <> + diff --git a/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx b/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx index 4afee0591f..75fac9d5b7 100644 --- a/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx +++ b/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx @@ -37,7 +37,9 @@ describe("LearningPathListingPage", () => { it("Has title 'Learning Paths'", async () => { setup() screen.getByRole("heading", { name: "Learning Paths" }) - await waitFor(() => expect(document.title).toBe("Learning Paths")) + await waitFor(() => + expect(document.title).toBe("Learning Paths | MIT Open"), + ) }) it("Renders a card for each learning path", async () => { diff --git a/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.tsx b/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.tsx index 604c428f58..5802a15c5e 100644 --- a/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.tsx +++ b/frontends/mit-open/src/pages/LearningPathListingPage/LearningPathListingPage.tsx @@ -93,9 +93,7 @@ const LearningPathListingPage: React.FC = () => { src="/static/images/course_search_banner.png" className="learningpaths-page" > - - Learning Paths - + diff --git a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts index b5d7082a69..1b7ba69ac3 100644 --- a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts +++ b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts @@ -71,7 +71,7 @@ describe("ListDetailsPage", () => { const path = factories.learningResources.learningPath() setup({ path }) await screen.findByRole("heading", { name: path.title }) - await waitFor(() => expect(document.title).toBe(path.title)) + await waitFor(() => expect(document.title).toBe(`${path.title} | MIT Open`)) }) test.each([ diff --git a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx index dd52015a22..4c78a70a6c 100644 --- a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx +++ b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx @@ -114,9 +114,7 @@ const ListDetailsPage: React.FC = ({ src="/static/images/course_search_banner.png" className="learningpaths-page" > - - {title} - + { return activeStep < NUM_STEPS ? ( - - Onboarding - +
diff --git a/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.test.tsx b/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.test.tsx index 201ec15e7c..c354c1dd61 100644 --- a/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.test.tsx +++ b/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.test.tsx @@ -18,7 +18,7 @@ describe("PrivacyPage", () => { url: commonUrls.PRIVACY, }) await waitFor(() => { - expect(document.title).toBe("Privacy Policy") + expect(document.title).toBe("Privacy Policy | MIT Open") }) screen.getByRole("heading", { name: "Privacy Policy", diff --git a/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.tsx b/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.tsx index 77a025c459..dc4ea0b08a 100644 --- a/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.tsx +++ b/frontends/mit-open/src/pages/PrivacyPage/PrivacyPage.tsx @@ -55,9 +55,7 @@ const PrivacyPage: React.FC = () => { return ( - - Privacy Policy - + { return ( <> - - Search - + diff --git a/frontends/mit-open/src/pages/TermsPage/TermsPage.test.tsx b/frontends/mit-open/src/pages/TermsPage/TermsPage.test.tsx index e2fa01ea5d..21d37d92da 100644 --- a/frontends/mit-open/src/pages/TermsPage/TermsPage.test.tsx +++ b/frontends/mit-open/src/pages/TermsPage/TermsPage.test.tsx @@ -18,7 +18,7 @@ describe("TermsPage", () => { url: commonUrls.TERMS, }) await waitFor(() => { - expect(document.title).toBe("Terms of Service") + expect(document.title).toBe("Terms of Service | MIT Open") }) screen.getByRole("heading", { name: "Terms of Service", diff --git a/frontends/mit-open/src/pages/TermsPage/TermsPage.tsx b/frontends/mit-open/src/pages/TermsPage/TermsPage.tsx index 316d29bb06..635844e448 100644 --- a/frontends/mit-open/src/pages/TermsPage/TermsPage.tsx +++ b/frontends/mit-open/src/pages/TermsPage/TermsPage.tsx @@ -59,9 +59,7 @@ const TermsPage: React.FC = () => { return ( - - Terms of Service - + { setupApis() renderWithProviders() await waitFor(() => { - expect(document.title).toBe("MIT Open | Topics") + expect(document.title).toBe("Topics | MIT Open") }) screen.getByRole("heading", { name: "Topics" }) }) diff --git a/frontends/mit-open/src/pages/TopicListingPage/TopicsListingPage.tsx b/frontends/mit-open/src/pages/TopicListingPage/TopicsListingPage.tsx index 705c307d1d..cf3644a0bd 100644 --- a/frontends/mit-open/src/pages/TopicListingPage/TopicsListingPage.tsx +++ b/frontends/mit-open/src/pages/TopicListingPage/TopicsListingPage.tsx @@ -278,9 +278,7 @@ const ToopicsListingPage: React.FC = () => { }, [topicsQuery.data?.results, courseQuery.data, programQuery.data]) return ( - - MIT Open | Topics - + { setupApis() renderWithProviders() await waitFor(() => { - expect(document.title).toBe("MIT Open | Units") + expect(document.title).toBe("Units | MIT Open") }) screen.getByRole("heading", { name: "Academic & Professional Learning" }) }) diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx index e27489140a..2506991e80 100644 --- a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx @@ -405,9 +405,7 @@ const UnitsListingPage: React.FC = () => { return ( - - MIT Open | Units - + { - it("Has title 'User Lists'", async () => { + it("Has heading 'User Lists' and correct page title", async () => { setup() screen.getByRole("heading", { name: "User Lists" }) - await waitFor(() => expect(document.title).toBe("User Lists")) + await waitFor(() => expect(document.title).toBe("My Lists | MIT Open")) }) it("Renders a card for each user list", async () => { diff --git a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx b/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx index 249bd03057..4f783cf40f 100644 --- a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx +++ b/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx @@ -164,9 +164,7 @@ const UserListListingPage: React.FC = () => { src="/static/images/course_search_banner.png" className="learningpaths-page" > - - User Lists - + { // within app, define process.env.VAR_NAME with default from cleanEnv MITOPEN_AXIOS_BASE_PATH, ENVIRONMENT, + SITE_NAME, }), ] .concat( diff --git a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx index 2a8564aa86..70d1f89d82 100644 --- a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx +++ b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx @@ -182,6 +182,7 @@ const NavDrawer = (props: NavDrawerProps) => { hideBackdrop={true} PaperProps={{ sx: { + borderRight: "none", boxShadow: "0px 6px 24px 0px rgba(37, 38, 43, 0.10)", zIndex: (theme) => theme.zIndex.appBar - 1, overscrollBehavior: "contain", diff --git a/frontends/ol-utilities/src/components/MetaTags.tsx b/frontends/ol-utilities/src/components/MetaTags.tsx index 7c94129e86..6564a0a933 100644 --- a/frontends/ol-utilities/src/components/MetaTags.tsx +++ b/frontends/ol-utilities/src/components/MetaTags.tsx @@ -2,6 +2,7 @@ import React from "react" import { Helmet } from "react-helmet-async" type MetaTagsProps = { + title?: string | string[] canonicalLink?: string children?: React.ReactNode } @@ -17,14 +18,22 @@ const getCanonicalUrl = (url: string): string => { /** * Renders a Helmet component to customize meta tags */ -const MetaTags: React.FC = ({ children, canonicalLink }) => - children || canonicalLink ? ( +const MetaTags: React.FC = ({ + title, + children, + canonicalLink, +}) => { + title = title ? (Array.isArray(title) ? title : [title]) : [] + + return ( + {[...title, process.env.SITE_NAME].join(" | ")} {children} {canonicalLink ? ( ) : null} - ) : null + ) +} export default MetaTags diff --git a/learning_resources/etl/loaders.py b/learning_resources/etl/loaders.py index 61a71c20c9..b2076c60a5 100644 --- a/learning_resources/etl/loaders.py +++ b/learning_resources/etl/loaders.py @@ -512,14 +512,24 @@ def load_programs( blocklist = load_course_blocklist() duplicates = load_course_duplicates(etl_source) - return [ - program - for program in [ - load_program(program_data, blocklist, duplicates, config=config) - for program_data in programs_data - ] - if program is not None + programs = [ + load_program(program_data, blocklist, duplicates, config=config) + for program_data in programs_data ] + if programs and config.prune: + for learning_resource in LearningResource.objects.filter( + etl_source=etl_source, resource_type=LearningResourceType.program.name + ).exclude( + id__in=[ + learning_resource.id + for learning_resource in programs + if learning_resource is not None + ] + ): + learning_resource.published = False + learning_resource.save() + resource_unpublished_actions(learning_resource) + return [program for program in programs if program is not None] def load_content_file( diff --git a/learning_resources/etl/loaders_test.py b/learning_resources/etl/loaders_test.py index e4472ef2ec..d33cd3a9bc 100644 --- a/learning_resources/etl/loaders_test.py +++ b/learning_resources/etl/loaders_test.py @@ -743,11 +743,14 @@ def test_load_courses(mocker, mock_blocklist, mock_duplicates, prune): def test_load_programs(mocker, mock_blocklist, mock_duplicates): """Test that load_programs calls the expected functions""" - program_data = [{"courses": [{"platform": "a"}, {}]}] + program_data = [{"courses": [{"platform": "a"}, {}], "id": 5}] + mock_load_program = mocker.patch( - "learning_resources.etl.loaders.load_program", autospec=True + "learning_resources.etl.loaders.load_program", + autospec=True, + return_value=ProgramFactory.create().learning_resource, ) - load_programs("mitx", program_data) + load_programs("mitx", program_data, config=ProgramLoaderConfig(prune=True)) assert mock_load_program.call_count == len(program_data) mock_blocklist.assert_called_once() mock_duplicates.assert_called_once_with("mitx") diff --git a/learning_resources/etl/mitxonline.py b/learning_resources/etl/mitxonline.py index 0a1a6ca697..e25ec6a283 100644 --- a/learning_resources/etl/mitxonline.py +++ b/learning_resources/etl/mitxonline.py @@ -90,7 +90,15 @@ def parse_page_attribute( def extract_programs(): """Loads the MITx Online catalog data""" # noqa: D401 if settings.MITX_ONLINE_PROGRAMS_API_URL: - return list(_fetch_data(settings.MITX_ONLINE_PROGRAMS_API_URL)) + return list( + _fetch_data( + settings.MITX_ONLINE_PROGRAMS_API_URL, + params={ + "page__live": True, + "live": True, + }, + ) + ) else: log.warning("Missing required setting MITX_ONLINE_PROGRAMS_API_URL") @@ -100,7 +108,15 @@ def extract_programs(): def extract_courses(): """Loads the MITx Online catalog data""" # noqa: D401 if settings.MITX_ONLINE_COURSES_API_URL: - return list(_fetch_data(settings.MITX_ONLINE_COURSES_API_URL)) + return list( + _fetch_data( + settings.MITX_ONLINE_COURSES_API_URL, + params={ + "page__live": True, + "live": True, + }, + ) + ) else: log.warning("Missing required setting MITX_ONLINE_COURSES_API_URL") @@ -210,6 +226,8 @@ def _transform_course(course): }, "published": bool( parse_page_attribute(course, "page_url") + and parse_page_attribute(course, "live") + and course.get("live", False) ), # a course is only considered published if it has a page url "professional": False, "certification": has_certification, @@ -244,7 +262,11 @@ def _fetch_courses_by_ids(course_ids): return list( _fetch_data( settings.MITX_ONLINE_COURSES_API_URL, - params={"id": ",".join([str(courseid) for courseid in course_ids])}, + params={ + "id": ",".join([str(courseid) for courseid in course_ids]), + "page__live": True, + "live": True, + }, ) ) diff --git a/learning_resources/etl/mitxonline_test.py b/learning_resources/etl/mitxonline_test.py index 333bfa582b..d1fc49f321 100644 --- a/learning_resources/etl/mitxonline_test.py +++ b/learning_resources/etl/mitxonline_test.py @@ -194,6 +194,8 @@ def test_mitxonline_transform_programs( ), "published": bool( course_data.get("page", {}).get("page_url", None) + and course_data.get("page", {}).get("live", None) + and course_data.get("live", None) ), "certification": True, "certification_type": CertificationType.completion.name, @@ -303,7 +305,11 @@ def test_mitxonline_transform_courses(settings, mock_mitxonline_courses_data): course_data.get("page", {}).get("description", None) ), "offered_by": OFFERED_BY, - "published": course_data.get("page", {}).get("page_url", None) is not None, + "published": bool( + course_data.get("page", {}).get("page_url", None) + and course_data.get("page", {}).get("live", None) + and course_data.get("live", None) + ), "professional": False, "certification": parse_certification( "mitx", diff --git a/learning_resources/etl/pipelines.py b/learning_resources/etl/pipelines.py index 90db29f0f6..7ccf48d062 100644 --- a/learning_resources/etl/pipelines.py +++ b/learning_resources/etl/pipelines.py @@ -53,7 +53,7 @@ mitxonline_programs_etl = compose( load_programs( ETLSource.mitxonline.name, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True), prune=True), ), mitxonline.transform_programs, mitxonline.extract_programs, diff --git a/learning_resources/etl/pipelines_test.py b/learning_resources/etl/pipelines_test.py index fb1525cca3..af94bda19d 100644 --- a/learning_resources/etl/pipelines_test.py +++ b/learning_resources/etl/pipelines_test.py @@ -79,7 +79,7 @@ def test_mitxonline_programs_etl(): mock_load_programs.assert_called_once_with( ETLSource.mitxonline.name, mock_transform.return_value, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True), prune=True), ) assert result == mock_load_programs.return_value diff --git a/learning_resources/management/commands/populate_featured_lists.py b/learning_resources/management/commands/populate_featured_lists.py index fe3c50cb32..cf106f3913 100644 --- a/learning_resources/management/commands/populate_featured_lists.py +++ b/learning_resources/management/commands/populate_featured_lists.py @@ -48,7 +48,7 @@ def handle(self, *args, **options): # noqa: ARG002 # Get the channel for the offeror unit_channel = FieldChannel.objects.filter( - unit_detail__offeror=offeror + unit_detail__unit=offeror ).first() if not unit_channel: self.stderr.write( @@ -89,6 +89,6 @@ def handle(self, *args, **options): # noqa: ARG002 total_seconds = (now_in_utc() - start).total_seconds() self.stdout.write( - "Population of offeror channel featured lists finished, " + "Population of unit channel featured lists finished, " f"took {total_seconds} seconds" ) diff --git a/learning_resources/models.py b/learning_resources/models.py index 72bbdb4780..0f0769dbaa 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -255,8 +255,11 @@ def prices(self) -> list[Decimal]: LearningResourceType.course.name, LearningResourceType.program.name, ]: - next_run = self.next_run - return next_run.prices if next_run else [] + next_run = ( + self.next_run + or self.runs.filter(published=True).order_by("-start_date").first() + ) + return next_run.prices if next_run and next_run.prices else [] else: return [Decimal(0.00)] diff --git a/learning_resources/models_test.py b/learning_resources/models_test.py index 4f0db4d45c..823105e1a4 100644 --- a/learning_resources/models_test.py +++ b/learning_resources/models_test.py @@ -1,10 +1,15 @@ """Tests for learning_resources.models""" +from datetime import timedelta +from decimal import Decimal + import pytest +from django.db.models import F from learning_resources.constants import LearningResourceType from learning_resources.factories import ( CourseFactory, + LearningResourceRunFactory, ProgramFactory, ) @@ -48,3 +53,30 @@ def test_course_creation(): assert resource.offered_by is not None assert resource.runs.count() == course.runs.count() assert resource.prices == resource.next_run.prices + + +def test_course_prices_current_no_next(): + """Test that course.prices == most recent run prices if no next run""" + course = CourseFactory.create() + resource = course.learning_resource + resource.runs.update(start_date=F("start_date") - timedelta(days=3650)) + unpub_run = LearningResourceRunFactory.create( + learning_resource=resource, published=False, prices=[Decimal("987654.32")] + ) + resource.refresh_from_db() + most_recent_run = resource.runs.filter(published=True).order_by("start_date").last() + assert most_recent_run != unpub_run + assert resource.next_run is None + # Prices should be from most recent published run if no next run + assert resource.prices == most_recent_run.prices + assert len(resource.prices) > 0 + + +def test_course_prices_unpublished_runs(): + """Test that course.prices == [] if no published run""" + course = CourseFactory.create() + resource = course.learning_resource + resource.runs.update(published=False) + resource.refresh_from_db() + assert resource.next_run is None + assert resource.prices == [] diff --git a/learning_resources_search/api.py b/learning_resources_search/api.py index 2f7fe833ce..4b6c20af70 100644 --- a/learning_resources_search/api.py +++ b/learning_resources_search/api.py @@ -38,7 +38,7 @@ LEARN_SUGGEST_FIELDS = ["title.trigram", "description.trigram"] COURSENUM_SORT_FIELD = "course.course_numbers.sort_coursenum" -DEFAULT_SORT = ["is_learning_material", "-created_on"] +DEFAULT_SORT = ["is_learning_material", "-views"] def gen_content_file_id(content_file_id): diff --git a/learning_resources_search/api_test.py b/learning_resources_search/api_test.py index 144b4b721d..ecc3bf6051 100644 --- a/learning_resources_search/api_test.py +++ b/learning_resources_search/api_test.py @@ -1762,7 +1762,7 @@ def test_document_percolation(opensearch, mocker): [ ("-views", None, [{"views": {"order": "desc"}}]), ("-views", "text", [{"views": {"order": "desc"}}]), - (None, None, ["is_learning_material", {"created_on": {"order": "desc"}}]), + (None, None, ["is_learning_material", {"views": {"order": "desc"}}]), (None, "text", None), ], ) diff --git a/main/settings.py b/main/settings.py index ffa3574854..a48c9eb9cc 100644 --- a/main/settings.py +++ b/main/settings.py @@ -33,7 +33,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.13.8" +VERSION = "0.13.9" log = logging.getLogger()