From f27d2ceb276a3c24e524be3a2086a6883d2583b0 Mon Sep 17 00:00:00 2001 From: Carey P Gumaer Date: Fri, 8 Nov 2024 12:35:29 -0500 Subject: [PATCH 01/16] learning resource drawer v2 run comparison table (#1782) * add first draft of run comparison table * hide labels on mobile * update differing run table styles * add tests * display table if location is different * move DifferingRunsTable to its own file * use getDisplayPrice * fix CTA image z-index issue * remove errant console.log * fix location display * remove duplicate sort --- .../LearningResourceCard/testUtils.ts | 84 ++++++++++- .../DifferingRunsTable.test.tsx | 39 +++++ .../DifferingRunsTable.tsx | 138 ++++++++++++++++++ .../InfoSectionV2.tsx | 14 +- .../LearningResourceExpandedV2.tsx | 1 + .../src/learning-resources/pricing.ts | 47 ++++-- 6 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.test.tsx create mode 100644 frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx diff --git a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts index 79eacb61b3..f6273497c1 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts +++ b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts @@ -1,4 +1,4 @@ -import { ResourceTypeEnum } from "api" +import { DeliveryEnum, DeliveryEnumDescriptions, ResourceTypeEnum } from "api" import { factories } from "api/test-utils" const _makeResource = factories.learningResources.resource @@ -41,6 +41,12 @@ const resources = { }), } +const sameDataRun = factories.learningResources.run({ + resource_prices: [ + { amount: "0", currency: "USD" }, + { amount: "100", currency: "USD" }, + ], +}) const courses = { free: { noCertificate: makeResource({ @@ -152,6 +158,82 @@ const courses = { availability: "dated", }), }, + multipleRuns: { + sameData: makeResource({ + resource_type: ResourceTypeEnum.Course, + runs: [ + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + ], + }), + differentData: makeResource({ + resource_type: ResourceTypeEnum.Course, + runs: [ + factories.learningResources.run({ + delivery: [ + { + code: DeliveryEnum.Online, + name: DeliveryEnumDescriptions.online, + }, + ], + resource_prices: [ + { amount: "0", currency: "USD" }, + { amount: "100", currency: "USD" }, + ], + }), + factories.learningResources.run({ + delivery: [ + { + code: DeliveryEnum.Online, + name: DeliveryEnumDescriptions.online, + }, + ], + resource_prices: [ + { amount: "0", currency: "USD" }, + { amount: "100", currency: "USD" }, + ], + }), + factories.learningResources.run({ + delivery: [ + { + code: DeliveryEnum.InPerson, + name: DeliveryEnumDescriptions.in_person, + }, + ], + resource_prices: [{ amount: "150", currency: "USD" }], + location: "Earth", + }), + factories.learningResources.run({ + delivery: [ + { + code: DeliveryEnum.InPerson, + name: DeliveryEnumDescriptions.in_person, + }, + ], + resource_prices: [{ amount: "150", currency: "USD" }], + location: "Earth", + }), + ], + }), + }, } const resourceArgType = { diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.test.tsx new file mode 100644 index 0000000000..99a832b9f0 --- /dev/null +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.test.tsx @@ -0,0 +1,39 @@ +import React from "react" +import { render, screen, within } from "@testing-library/react" +import { courses } from "../LearningResourceCard/testUtils" +import InfoSectionV2 from "./InfoSectionV2" +import { ThemeProvider } from "../ThemeProvider/ThemeProvider" +import { DeliveryEnumDescriptions } from "api" + +describe("Differing runs comparison table", () => { + test("Does not appear if data is the same", () => { + const course = courses.multipleRuns.sameData + render(, { + wrapper: ThemeProvider, + }) + expect(screen.queryByTestId("differing-runs-table")).toBeNull() + }) + + test("Appears if data is different", () => { + const course = courses.multipleRuns.differentData + render(, { + wrapper: ThemeProvider, + }) + const differingRunsTable = screen.getByTestId("differing-runs-table") + expect(differingRunsTable).toBeInTheDocument() + const onlineLabels = within(differingRunsTable).getAllByText( + DeliveryEnumDescriptions.online, + ) + const inPersonLabels = within(differingRunsTable).getAllByText( + DeliveryEnumDescriptions.in_person, + ) + const onlinePriceLabels = within(differingRunsTable).getAllByText("$100") + const inPersonPriceLabels = within(differingRunsTable).getAllByText("$150") + const earthLocationLabels = within(differingRunsTable).getAllByText("Earth") + expect(onlineLabels).toHaveLength(2) + expect(inPersonLabels).toHaveLength(2) + expect(onlinePriceLabels).toHaveLength(2) + expect(inPersonPriceLabels).toHaveLength(2) + expect(earthLocationLabels).toHaveLength(2) + }) +}) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx new file mode 100644 index 0000000000..179297d187 --- /dev/null +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx @@ -0,0 +1,138 @@ +import React from "react" +import styled from "@emotion/styled" +import { theme } from "../ThemeProvider/ThemeProvider" +import { LearningResource, LearningResourcePrice } from "api" +import { + formatRunDate, + getDisplayPrice, + getRunPrices, + showStartAnytime, +} from "ol-utilities" + +const DifferingRuns = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + alignSelf: "stretch", + borderRadius: "4px", + border: `1px solid ${theme.custom.colors.lightGray2}`, + borderBottom: "none", +}) + +const DifferingRun = styled.div({ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + gap: "16px", + padding: "12px", + alignSelf: "stretch", + borderBottom: `1px solid ${theme.custom.colors.lightGray2}`, +}) + +const DifferingRunHeader = styled.div({ + display: "flex", + alignSelf: "stretch", + alignItems: "center", + flex: "1 0 0", + gap: "16px", + padding: "12px", + color: theme.custom.colors.darkGray2, + backgroundColor: theme.custom.colors.lightGray1, + ...theme.typography.subtitle3, +}) + +const DifferingRunData = styled.div({ + display: "flex", + flexShrink: 0, + flex: "1 0 0", + color: theme.custom.colors.darkGray2, + ...theme.typography.body3, +}) + +const DifferingRunLabel = styled.strong({ + display: "flex", + flex: "1 0 0", +}) + +const DifferingRunLocation = styled(DifferingRunData)({ + flex: "1 0 100%", + flexDirection: "column", + alignSelf: "stretch", +}) + +const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({ + resource, +}) => { + if (!resource.runs) { + return null + } + if (resource.runs.length === 1) { + return null + } + const asTaughtIn = resource ? showStartAnytime(resource) : false + const prices: LearningResourcePrice[] = [] + const deliveryMethods = [] + const locations = [] + for (const run of resource.runs) { + if (run.resource_prices) { + run.resource_prices.forEach((price) => { + if (price.amount !== "0") { + prices.push(price) + } + }) + } + if (run.delivery) { + deliveryMethods.push(run.delivery) + } + if (run.location) { + locations.push(run.location) + } + } + const distinctPrices = [...new Set(prices.map((p) => p.amount).flat())] + const distinctDeliveryMethods = [ + ...new Set(deliveryMethods.flat().map((dm) => dm?.code)), + ] + const distinctLocations = [...new Set(locations.flat().map((l) => l))] + if ( + distinctPrices.length > 1 || + distinctDeliveryMethods.length > 1 || + distinctLocations.length > 1 + ) { + return ( + + + Date + Price + Format + + {resource.runs.map((run, index) => ( + + + {formatRunDate(run, asTaughtIn)} + + {run.resource_prices && ( + + {getDisplayPrice(getRunPrices(run)["course"])} + + )} + {run.delivery && ( + + {run.delivery?.map((dm) => dm?.name).join(", ")} + + )} + {run.delivery.filter((d) => d.code === "in_person").length > 0 && + run.location && ( + + Location + {run.location} + + )} + + ))} + + ) + } + return null +} + +export default DifferingRunsTable diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 6c27cf378a..d0464760bb 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -22,6 +22,7 @@ import { showStartAnytime, } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" +import DifferingRunsTable from "./DifferingRunsTable" const SeparatorContainer = styled.span({ padding: "0 8px", @@ -392,11 +393,14 @@ const InfoSectionV2 = ({ resource }: { resource?: LearningResource }) => { } return ( - - {infoItems.map((props, index) => ( - - ))} - + <> + + + {infoItems.map((props, index) => ( + + ))} + + ) } diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx index d273f2f9cb..4a3759b218 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx @@ -88,6 +88,7 @@ const Image = styled(NextImage)({ borderRadius: "8px", width: "100%", objectFit: "cover", + zIndex: -1, }) const SkeletonImage = styled(Skeleton)<{ aspect: number }>((aspect) => ({ diff --git a/frontends/ol-utilities/src/learning-resources/pricing.ts b/frontends/ol-utilities/src/learning-resources/pricing.ts index f8f5867a98..7b13bd0e75 100644 --- a/frontends/ol-utilities/src/learning-resources/pricing.ts +++ b/frontends/ol-utilities/src/learning-resources/pricing.ts @@ -1,4 +1,9 @@ -import { LearningResource, LearningResourcePrice, ResourceTypeEnum } from "api" +import { + LearningResource, + LearningResourcePrice, + LearningResourceRun, + ResourceTypeEnum, +} from "api" import { findBestRun } from "ol-utilities" import getSymbolFromCurrency from "currency-symbol-map" @@ -30,20 +35,23 @@ type Prices = { certificate: null | LearningResourcePrice[] } -const getPrices = (resource: LearningResource): Prices => { - const sortedNonzero = resource.resource_prices - ? resource.resource_prices - .sort( - (a: LearningResourcePrice, b: LearningResourcePrice) => - Number(a.amount) - Number(b.amount), - ) - .filter((price: LearningResourcePrice) => Number(price.amount) > 0) - : [] - +const getPrices = (prices: LearningResourcePrice[]) => { + const sortedNonzero = prices + .sort( + (a: LearningResourcePrice, b: LearningResourcePrice) => + Number(a.amount) - Number(b.amount), + ) + .filter((price: LearningResourcePrice) => Number(price.amount) > 0) const priceRange = sortedNonzero.filter( (price, index, arr) => index === 0 || index === arr.length - 1, ) - const prices = priceRange.length > 0 ? priceRange : null + return priceRange.length > 0 ? priceRange : null +} + +const getResourcePrices = (resource: LearningResource): Prices => { + const prices = resource.resource_prices + ? getPrices(resource.resource_prices) + : [] if (resource.free) { return resource.certification @@ -56,6 +64,15 @@ const getPrices = (resource: LearningResource): Prices => { } } +export const getRunPrices = (run: LearningResourceRun): Prices => { + const prices = run.resource_prices ? getPrices(run.resource_prices) : [] + + return { + course: prices ?? PAID, + certificate: null, + } +} + const getDisplayPrecision = (price: number) => { if (Number.isInteger(price)) { return price.toFixed(0) @@ -63,7 +80,9 @@ const getDisplayPrecision = (price: number) => { return price.toFixed(2) } -const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => { +export const getDisplayPrice = ( + price: Prices["course"] | Prices["certificate"], +) => { if (price === null) { return null } @@ -82,7 +101,7 @@ const getDisplayPrice = (price: Prices["course"] | Prices["certificate"]) => { } export const getLearningResourcePrices = (resource: LearningResource) => { - const prices = getPrices(resource) + const prices = getResourcePrices(resource) return { course: { value: prices.course, From b4c8956749badc6fad68b88436615d398958ed8b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:25:00 -0600 Subject: [PATCH 02/16] Update dependency @chromatic-com/storybook to v3 (#1764) --- frontends/ol-components/package.json | 2 +- yarn.lock | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/frontends/ol-components/package.json b/frontends/ol-components/package.json index 78045e5752..82c0ee2a74 100644 --- a/frontends/ol-components/package.json +++ b/frontends/ol-components/package.json @@ -45,7 +45,7 @@ "validator": "^13.11.0" }, "devDependencies": { - "@chromatic-com/storybook": "^1.9.0", + "@chromatic-com/storybook": "^3.0.0", "@faker-js/faker": "^9.0.0", "@storybook/addon-actions": "^8.2.9", "@storybook/addon-essentials": "^8.2.9", diff --git a/yarn.lock b/yarn.lock index e1fcaba9be..2e64ec07c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1463,16 +1463,18 @@ __metadata: languageName: node linkType: hard -"@chromatic-com/storybook@npm:^1.9.0": - version: 1.9.0 - resolution: "@chromatic-com/storybook@npm:1.9.0" +"@chromatic-com/storybook@npm:^3.0.0": + version: 3.2.2 + resolution: "@chromatic-com/storybook@npm:3.2.2" dependencies: - chromatic: "npm:^11.4.0" + chromatic: "npm:^11.15.0" filesize: "npm:^10.0.12" jsonfile: "npm:^6.1.0" react-confetti: "npm:^6.1.0" strip-ansi: "npm:^7.1.0" - checksum: 10/27ca6930a4978a52471ed7256cbf549e57b5c9c45b650b55461400a63692f5b30a7a0a7436faadd713952ce6285b873041494c10e92cccdc5bdafee1f1755459 + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + checksum: 10/71338edf56cdbc855074c78981f2e1612b364cd864fa99bbda5c0aad147769b9f476de2fd76816102fd504efc5c0c54ba559d5ac9e3828d53278fe7000863d54 languageName: node linkType: hard @@ -8439,9 +8441,9 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^11.4.0": - version: 11.12.5 - resolution: "chromatic@npm:11.12.5" +"chromatic@npm:^11.15.0": + version: 11.16.5 + resolution: "chromatic@npm:11.16.5" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 @@ -8454,7 +8456,7 @@ __metadata: chroma: dist/bin.js chromatic: dist/bin.js chromatic-cli: dist/bin.js - checksum: 10/1b67b8f1c3813871453910a3d90b1dfe20da9ecd0fc71bb5186b132d72fc3a11a6e74f14abd1b51e1ebbd99bd7089e1615ba7be8529be5dde7897b9778c48ae7 + checksum: 10/df200f3900be2b5bf662f2308b15693730d55380085636c20ccd7a78913b3b7e5401d32706098462e020542dc47997021ce894e17fb4bbf771a48b73378884c2 languageName: node linkType: hard @@ -16017,7 +16019,7 @@ __metadata: version: 0.0.0-use.local resolution: "ol-components@workspace:frontends/ol-components" dependencies: - "@chromatic-com/storybook": "npm:^1.9.0" + "@chromatic-com/storybook": "npm:^3.0.0" "@dnd-kit/core": "npm:^6.0.8" "@dnd-kit/sortable": "npm:^8.0.0" "@dnd-kit/utilities": "npm:^3.2.1" From 7ddecb8ea74d0103ca463e1c5ae9fc3133bb4e51 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:16:50 +0000 Subject: [PATCH 03/16] Update dependency ruff to v0.7.3 (#1810) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- poetry.lock | 40 ++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4bb078c4a4..db95fc6225 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5129,29 +5129,29 @@ files = [ [[package]] name = "ruff" -version = "0.7.2" +version = "0.7.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8"}, - {file = "ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4"}, - {file = "ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80"}, - {file = "ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088"}, - {file = "ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748"}, - {file = "ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828"}, - {file = "ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e"}, - {file = "ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691"}, - {file = "ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8"}, - {file = "ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88"}, - {file = "ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760"}, - {file = "ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f"}, + {file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"}, + {file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"}, + {file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"}, + {file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"}, + {file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"}, + {file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"}, + {file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"}, + {file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"}, + {file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"}, ] [[package]] @@ -6094,4 +6094,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "3.12.6" -content-hash = "c4ab6db22daf236399f7726c5e76af7a7009cbb8f26779eff4ebc4118697ad38" +content-hash = "da8cfbd5627071399d2e10b73fc4566fb3da0eee7fde48217e323036f2c66632" diff --git a/pyproject.toml b/pyproject.toml index 297fd683b4..2af57b2ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ django-scim2 = "^0.19.1" django-oauth-toolkit = "^2.3.0" youtube-transcript-api = "^0.6.2" posthog = "^3.5.0" -ruff = "0.7.2" +ruff = "0.7.3" dateparser = "^1.2.0" uwsgitop = "^0.12" pytest-lazy-fixtures = "^1.1.1" From fa3b660c3210b7e8a5e79a53f48244230ef5dc15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:18:06 +0000 Subject: [PATCH 04/16] Update dependency postcss-styled-syntax to ^0.7.0 (#1811) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- frontends/package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontends/package.json b/frontends/package.json index 7f5deed23e..2d6b861090 100644 --- a/frontends/package.json +++ b/frontends/package.json @@ -67,7 +67,7 @@ "jest-fail-on-console": "^3.2.0", "jest-watch-typeahead": "^2.2.2", "jest-when": "^3.6.0", - "postcss-styled-syntax": "^0.6.4", + "postcss-styled-syntax": "^0.7.0", "prettier": "^3.3.3", "prettier-plugin-django-alpine": "^1.2.6", "stylelint": "^15.2.0", diff --git a/yarn.lock b/yarn.lock index 2e64ec07c5..4511e2786a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11410,7 +11410,7 @@ __metadata: jest-fail-on-console: "npm:^3.2.0" jest-watch-typeahead: "npm:^2.2.2" jest-when: "npm:^3.6.0" - postcss-styled-syntax: "npm:^0.6.4" + postcss-styled-syntax: "npm:^0.7.0" prettier: "npm:^3.3.3" prettier-plugin-django-alpine: "npm:^1.2.6" stylelint: "npm:^15.2.0" @@ -17121,14 +17121,14 @@ __metadata: languageName: node linkType: hard -"postcss-styled-syntax@npm:^0.6.4": - version: 0.6.4 - resolution: "postcss-styled-syntax@npm:0.6.4" +"postcss-styled-syntax@npm:^0.7.0": + version: 0.7.0 + resolution: "postcss-styled-syntax@npm:0.7.0" dependencies: - typescript: "npm:^5.3.3" + typescript: "npm:^5.6.3" peerDependencies: postcss: ^8.4.21 - checksum: 10/435bc414cc68d2d2297bb6354230226f554b88efb38610c0c35575eefc241edbabf12a4d4662957204e92091443fa25c85cb943916b61023c222e30a1127b1ff + checksum: 10/d52b4d556baf6b3c700fbb6b71bcc5bd3787d9af05a781bc1050031f61ac8e3ac60c5f7b8e924bd7ce6f8ff6b7d3314533f1526dd804729abdd021f835e35235 languageName: node linkType: hard @@ -20293,7 +20293,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5, typescript@npm:^5.3.3, typescript@npm:^5.4.3, typescript@npm:^5.5.4": +"typescript@npm:^5, typescript@npm:^5.4.3, typescript@npm:^5.5.4, typescript@npm:^5.6.3": version: 5.6.3 resolution: "typescript@npm:5.6.3" bin: @@ -20303,7 +20303,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": +"typescript@patch:typescript@npm%3A^5#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin, typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": version: 5.6.3 resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" bin: From 82a09762932a57cef30b2c1a85b14f3e6bafcf4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:56:16 +0000 Subject: [PATCH 05/16] Update opensearchproject/opensearch Docker tag to v2.18.0 (#1812) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.opensearch.base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.opensearch.base.yml b/docker-compose.opensearch.base.yml index 05741b9bfa..f722ca7a47 100644 --- a/docker-compose.opensearch.base.yml +++ b/docker-compose.opensearch.base.yml @@ -1,6 +1,6 @@ services: opensearch: - image: opensearchproject/opensearch:2.17.1 + image: opensearchproject/opensearch:2.18.0 environment: - "cluster.name=opensearch-cluster" - "bootstrap.memory_lock=true" # along with the memlock settings below, disables swapping From da4b0e7b6566fedb1d3f651668fe9e65ed28214b Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Tue, 12 Nov 2024 08:35:28 -0500 Subject: [PATCH 06/16] Clear resource_type filter when leaving Learning Materials search tab (#1780) --- .../app-pages/SearchPage/SearchPage.test.tsx | 26 +++++++++++++++++++ .../SearchDisplay/ResourceCategoryTabs.tsx | 3 +++ 2 files changed, 29 insertions(+) diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index 20a743990c..d5c926cb09 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -491,6 +491,32 @@ describe("Search Page Tabs", () => { expect(params2.get("department")).toBe("8") // should preserve other params }) + test("Switching from learning materials tab clears resource type only", async () => { + setMockApiResponses({ + search: { + count: 1000, + metadata: { + aggregations: { + resource_type: [{ key: "video", doc_count: 100 }], + }, + suggestions: [], + }, + }, + }) + const { location } = renderWithProviders(, { + url: "?resource_category=learning_material&resource_type=video&topic=Biology", + }) + const tabLM = screen.getByRole("tab", { name: /Learning Materials/ }) + const tabCourses = screen.getByRole("tab", { name: /Courses/ }) + expect(tabLM).toHaveAttribute("aria-selected") + + // Click "Courses" + await user.click(tabCourses) + expect(location.current.search).toBe( + "?resource_category=course&topic=Biology", + ) + }) + test("Tab titles show corret result counts", async () => { setMockApiResponses({ search: { diff --git a/frontends/main/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx b/frontends/main/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx index 3edd248b96..cefd652e24 100644 --- a/frontends/main/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx +++ b/frontends/main/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx @@ -94,6 +94,9 @@ const ResourceCategoryTabList: React.FC = ({ const tab = tabs.find((t) => t.name === value) setSearchParams((prev) => { const next = new URLSearchParams(prev) + if (prev.get("resource_category") === "learning_material") { + next.delete("resource_type") + } if (tab?.resource_category) { next.set("resource_category", tab.resource_category) } else { From a9ef1ba35715ff106994d745679b77e0d971adf2 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Tue, 12 Nov 2024 16:18:40 -0500 Subject: [PATCH 07/16] Endpoints for userlist/learningpath memberships (#1808) --- frontends/api/src/generated/v1/api.ts | 186 ++++++++++++++++++ learning_resources/permissions.py | 9 + learning_resources/urls.py | 10 + learning_resources/views.py | 54 +++++ learning_resources/views_learningpath_test.py | 22 +++ learning_resources/views_userlist_test.py | 24 +++ openapi/specs/v1.yaml | 32 +++ 7 files changed, 337 insertions(+) diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 99e09952af..c49bbf18d3 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -18233,6 +18233,45 @@ export const LearningpathsApiAxiosParamCreator = function ( options: localVarRequestOptions, } }, + /** + * Get a list of all learning path items + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningpathsMembershipList: async ( + options: RawAxiosRequestConfig = {}, + ): Promise => { + const localVarPath = `/api/v1/learningpaths/membership/` + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) + let baseOptions + if (configuration) { + baseOptions = configuration.baseOptions + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + } + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, /** * Update individual fields of a learning path * @summary Update @@ -18672,6 +18711,35 @@ export const LearningpathsApiFp = function (configuration?: Configuration) { configuration, )(axios, operationBasePath || basePath) }, + /** + * Get a list of all learning path items + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async learningpathsMembershipList( + options?: RawAxiosRequestConfig, + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string, + ) => AxiosPromise> + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.learningpathsMembershipList(options) + const index = configuration?.serverIndex ?? 0 + const operationBasePath = + operationServerMap["LearningpathsApi.learningpathsMembershipList"]?.[ + index + ]?.url + return (axios, basePath) => + createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration, + )(axios, operationBasePath || basePath) + }, /** * Update individual fields of a learning path * @summary Update @@ -18917,6 +18985,19 @@ export const LearningpathsApiFactory = function ( ) .then((request) => request(axios, basePath)) }, + /** + * Get a list of all learning path items + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningpathsMembershipList( + options?: RawAxiosRequestConfig, + ): AxiosPromise> { + return localVarFp + .learningpathsMembershipList(options) + .then((request) => request(axios, basePath)) + }, /** * Update individual fields of a learning path * @summary Update @@ -19456,6 +19537,19 @@ export class LearningpathsApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)) } + /** + * Get a list of all learning path items + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LearningpathsApi + */ + public learningpathsMembershipList(options?: RawAxiosRequestConfig) { + return LearningpathsApiFp(this.configuration) + .learningpathsMembershipList(options) + .then((request) => request(this.axios, this.basePath)) + } + /** * Update individual fields of a learning path * @summary Update @@ -24126,6 +24220,45 @@ export const UserlistsApiAxiosParamCreator = function ( options: localVarRequestOptions, } }, + /** + * Get a list of all userlist items for a user + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + userlistsMembershipList: async ( + options: RawAxiosRequestConfig = {}, + ): Promise => { + const localVarPath = `/api/v1/userlists/membership/` + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) + let baseOptions + if (configuration) { + baseOptions = configuration.baseOptions + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + } + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, /** * Viewset for UserLists * @summary Update @@ -24504,6 +24637,33 @@ export const UserlistsApiFp = function (configuration?: Configuration) { configuration, )(axios, operationBasePath || basePath) }, + /** + * Get a list of all userlist items for a user + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async userlistsMembershipList( + options?: RawAxiosRequestConfig, + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string, + ) => AxiosPromise> + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.userlistsMembershipList(options) + const index = configuration?.serverIndex ?? 0 + const operationBasePath = + operationServerMap["UserlistsApi.userlistsMembershipList"]?.[index]?.url + return (axios, basePath) => + createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration, + )(axios, operationBasePath || basePath) + }, /** * Viewset for UserLists * @summary Update @@ -24722,6 +24882,19 @@ export const UserlistsApiFactory = function ( ) .then((request) => request(axios, basePath)) }, + /** + * Get a list of all userlist items for a user + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + userlistsMembershipList( + options?: RawAxiosRequestConfig, + ): AxiosPromise> { + return localVarFp + .userlistsMembershipList(options) + .then((request) => request(axios, basePath)) + }, /** * Viewset for UserLists * @summary Update @@ -25127,6 +25300,19 @@ export class UserlistsApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)) } + /** + * Get a list of all userlist items for a user + * @summary List + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UserlistsApi + */ + public userlistsMembershipList(options?: RawAxiosRequestConfig) { + return UserlistsApiFp(this.configuration) + .userlistsMembershipList(options) + .then((request) => request(this.axios, this.basePath)) + } + /** * Viewset for UserLists * @summary Update diff --git a/learning_resources/permissions.py b/learning_resources/permissions.py index d5a6054eb2..50deec908d 100644 --- a/learning_resources/permissions.py +++ b/learning_resources/permissions.py @@ -47,6 +47,15 @@ def has_object_permission(self, request, view, obj): # noqa: ARG002 return can_edit +class HasLearningPathMembershipPermissions(BasePermission): + """ + Permission to view all LearningPath memberships + """ + + def has_permission(self, request, view): # noqa: ARG002 + return is_admin_user(request) or is_learning_path_editor(request) + + class HasLearningPathItemPermissions(BasePermission): """Permission to view/create/modify LearningPathItems""" diff --git a/learning_resources/urls.py b/learning_resources/urls.py index 6a770de2a5..29bd8ec8c2 100644 --- a/learning_resources/urls.py +++ b/learning_resources/urls.py @@ -100,6 +100,16 @@ router.register(r"offerors", views.OfferedByViewSet, basename="offerors_api") v1_urls = [ + path( + "learningpaths/membership/", + views.LearningPathMembershipViewSet.as_view({"get": "list"}), + name="learningpaths_api-membership", + ), + path( + "userlists/membership/", + views.UserListMembershipViewSet.as_view({"get": "list"}), + name="userlists_api-membership", + ), *router.urls, *nested_learning_resources_router.urls, *nested_courses_router.urls, diff --git a/learning_resources/views.py b/learning_resources/views.py index 550eb083ab..459a4ac1a6 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -18,6 +18,7 @@ from rest_framework.filters import OrderingFilter from rest_framework.generics import get_object_or_404 from rest_framework.pagination import LimitOffsetPagination +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework_nested.viewsets import NestedViewSetMixin @@ -71,6 +72,8 @@ LearningResourceSchoolSerializer, LearningResourceSerializer, LearningResourceTopicSerializer, + MicroLearningPathRelationshipSerializer, + MicroUserListRelationshipSerializer, PodcastEpisodeResourceSerializer, PodcastResourceSerializer, ProgramResourceSerializer, @@ -428,6 +431,32 @@ def get_queryset(self): return queryset +@extend_schema_view( + list=extend_schema( + summary="List", description="Get a list of all learning path items" + ), +) +class LearningPathMembershipViewSet(viewsets.ReadOnlyModelViewSet): + """Viewset for listing all learning path relationships""" + + serializer_class = MicroLearningPathRelationshipSerializer + permission_classes = (permissions.HasLearningPathMembershipPermissions,) + http_method_names = ["get"] + + def get_queryset(self): + """ + Generate a QuerySet for fetching all LearningResourceRelationships + with a parent of resource type "learning_path" + + Returns: + QuerySet of LearningResourceRelationships objects with learning path parents + """ + return LearningResourceRelationship.objects.filter( + child__published=True, + parent__resource_type=LearningResourceType.learning_path.name, + ).order_by("child", "parent") + + @extend_schema_view( list=extend_schema( summary="Nested Learning Resource List", @@ -872,6 +901,31 @@ def podcast_rss_feed(request): # noqa: ARG001 ) +@extend_schema_view( + list=extend_schema( + summary="List", description="Get a list of all userlist items for a user" + ), +) +class UserListMembershipViewSet(viewsets.ReadOnlyModelViewSet): + """Viewset for all user list relationships""" + + serializer_class = MicroUserListRelationshipSerializer + permission_classes = (IsAuthenticated,) + http_method_names = ["get"] + + def get_queryset(self): + """ + Generate a QuerySet for fetching all UserListRelationships for the user + + Returns: + QuerySet of UserListRelationship objects authored by the user + """ + return UserListRelationship.objects.filter( + child__published=True, + parent__author=self.request.user, + ).order_by("child", "parent") + + @method_decorator(blocked_ip_exempt, name="dispatch") class WebhookOCWView(views.APIView): """ diff --git a/learning_resources/views_learningpath_test.py b/learning_resources/views_learningpath_test.py index f3214fdd43..a7b1d032f5 100644 --- a/learning_resources/views_learningpath_test.py +++ b/learning_resources/views_learningpath_test.py @@ -360,6 +360,28 @@ def test_learning_path_endpoint_delete(client, user, is_editor): ) +@pytest.mark.parametrize("is_editor", [True, False]) +def test_learning_path_endpoint_membership_get(client, user, is_editor): + """Test learning path membership endpoint""" + update_editor_group(user, is_editor) + learning_paths = factories.LearningResourceFactory.create_batch( + 3, is_learning_path=True + ) + relationships = models.LearningResourceRelationship.objects.filter( + parent__in=learning_paths + ).order_by("child", "parent") + + client.force_login(user) + resp = client.get(reverse("lr:v1:learningpaths_api-membership")) + if is_editor: + assert len(resp.data) == relationships.count() + for idx, relationship in enumerate(relationships): + assert resp.data[idx]["parent"] == relationship.parent_id + assert resp.data[idx]["child"] == relationship.child_id + else: + assert resp.status_code == 403 + + @pytest.mark.parametrize("is_editor", [True, False]) def test_get_resource_learning_paths(user_client, user, is_editor): """Test that the learning paths are returned for a resource""" diff --git a/learning_resources/views_userlist_test.py b/learning_resources/views_userlist_test.py index ff8679b09d..773efd4d81 100644 --- a/learning_resources/views_userlist_test.py +++ b/learning_resources/views_userlist_test.py @@ -112,6 +112,30 @@ def test_user_list_endpoint_patch(client, update_topics): ) +@pytest.mark.parametrize("is_authenticated", [True, False]) +def test_user_list_endpoint_membership_get(client, user, is_authenticated): + """Test user list membership endpoint""" + factories.UserListRelationshipFactory.create_batch( + 3, parent=factories.UserListFactory.create(author=user) + ) + factories.UserListRelationshipFactory.create_batch(2) + + relationships = UserListRelationship.objects.filter(parent__author=user).order_by( + "child", "parent" + ) + assert relationships.count() == 3 + if is_authenticated: + client.force_login(user) + resp = client.get(reverse("lr:v1:userlists_api-membership")) + if is_authenticated: + assert len(resp.data) == relationships.count() + for idx, relationship in enumerate(relationships): + assert resp.data[idx]["parent"] == relationship.parent_id + assert resp.data[idx]["child"] == relationship.child_id + else: + assert resp.status_code == 403 + + @pytest.mark.parametrize("is_author", [True, False]) def test_user_list_items_endpoint_create_item(client, user, is_author): """Test userlistitems endpoint for creating a UserListItem""" diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 120ae561ee..5be827d60f 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -5730,6 +5730,22 @@ paths: responses: '204': description: No response body + /api/v1/learningpaths/membership/: + get: + operationId: learningpaths_membership_list + description: Get a list of all learning path items + summary: List + tags: + - learningpaths + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MicroLearningPathRelationship' + description: '' /api/v1/offerors/: get: operationId: offerors_list @@ -7515,6 +7531,22 @@ paths: responses: '204': description: No response body + /api/v1/userlists/membership/: + get: + operationId: userlists_membership_list + description: Get a list of all userlist items for a user + summary: List + tags: + - userlists + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MicroUserListRelationship' + description: '' /api/v1/video_playlists/: get: operationId: video_playlists_list From 2942facd3e3a67489d915deda359faafa962d565 Mon Sep 17 00:00:00 2001 From: James Kachel Date: Wed, 13 Nov 2024 10:34:20 -0600 Subject: [PATCH 08/16] Add data-ph- elements to CTA buttons (#1821) --- .../LearningResourceExpandedV1.test.tsx | 1 + .../LearningResourceExpanded/LearningResourceExpandedV1.tsx | 4 ++++ .../LearningResourceExpandedV2.test.tsx | 1 + .../LearningResourceExpanded/LearningResourceExpandedV2.tsx | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.test.tsx index 8a1a54df9d..122e7f49ea 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.test.tsx @@ -104,6 +104,7 @@ describe("Learning Resource Expanded", () => { }) as HTMLAnchorElement expect(link.href).toMatch(new RegExp(`^${resource.url}/?$`)) + expect(link.getAttribute("data-ph-action")).toBe("click-cta") } }, ) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx index a0155829db..09005f595a 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx @@ -244,6 +244,10 @@ const CallToActionSection = ({ } href={getCallToActionUrl(resource) || ""} > diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.test.tsx index 0f242e754e..3af8b4f2c2 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.test.tsx @@ -105,6 +105,7 @@ describe("Learning Resource Expanded", () => { }) as HTMLAnchorElement expect(link.href).toMatch(new RegExp(`^${resource.url}/?$`)) + expect(link.getAttribute("data-ph-action")).toBe("click-cta") } }, ) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx index 4a3759b218..f05fc4e9a6 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx @@ -355,6 +355,10 @@ const CallToActionSection = ({ } href={getCallToActionUrl(resource) || ""} > From 41d284d931b58efaea7f6d4c6f4751bd6f8e384d Mon Sep 17 00:00:00 2001 From: Carey P Gumaer Date: Wed, 13 Nov 2024 12:05:20 -0500 Subject: [PATCH 09/16] show more button for v2 drawer dates (#1809) * break out learning resource run comparison into a utility function * add show more functionality for start dates * remove separator at the end before show more / show less button * don't wrap dates * don't wrap show more button * fix test * fix cta image, use aspect-ratio and relative position * put aspect on img element instead of containing div so it works in chrome and safari * fix tests * remove start date comparison as it's not supposed to work that way * don't show dates info item at all if there are differning runs * show less link should be on its own line * remove unnecessary line break * optimize the code a bit * better name * fix column widths in differing runs table * refactor logic surrounding run dates * filter out null start dates * adjust padding above "Show less" --- .../LearningResourceCard/testUtils.ts | 7 + .../DifferingRunsTable.tsx | 94 ++++++------- .../InfoSectionV2.test.tsx | 35 ++++- .../InfoSectionV2.tsx | 127 +++++++++++++----- .../LearningResourceExpandedV2.tsx | 26 ++-- .../learning-resources.test.ts | 127 +++++++++++++++++- .../learning-resources/learning-resources.ts | 49 ++++++- 7 files changed, 365 insertions(+), 100 deletions(-) diff --git a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts index f6273497c1..c1dfaf11c1 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts +++ b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts @@ -42,6 +42,12 @@ const resources = { } const sameDataRun = factories.learningResources.run({ + delivery: [ + { + code: DeliveryEnum.Online, + name: DeliveryEnumDescriptions.online, + }, + ], resource_prices: [ { amount: "0", currency: "USD" }, { amount: "100", currency: "USD" }, @@ -161,6 +167,7 @@ const courses = { multipleRuns: { sameData: makeResource({ resource_type: ResourceTypeEnum.Course, + free: true, runs: [ factories.learningResources.run({ delivery: sameDataRun.delivery, diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx index 179297d187..57bdaf0843 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx @@ -1,8 +1,9 @@ import React from "react" import styled from "@emotion/styled" import { theme } from "../ThemeProvider/ThemeProvider" -import { LearningResource, LearningResourcePrice } from "api" +import { LearningResource } from "api" import { + allRunsAreIdentical, formatRunDate, getDisplayPrice, getRunPrices, @@ -33,7 +34,6 @@ const DifferingRunHeader = styled.div({ display: "flex", alignSelf: "stretch", alignItems: "center", - flex: "1 0 0", gap: "16px", padding: "12px", color: theme.custom.colors.darkGray2, @@ -43,17 +43,46 @@ const DifferingRunHeader = styled.div({ const DifferingRunData = styled.div({ display: "flex", - flexShrink: 0, - flex: "1 0 0", color: theme.custom.colors.darkGray2, ...theme.typography.body3, }) const DifferingRunLabel = styled.strong({ display: "flex", - flex: "1 0 0", }) +const dateColumnStyle = { + width: "130px", + [theme.breakpoints.down("sm")]: { + width: "auto", + flex: "2 0 0", + }, +} + +const priceColumnStyle = { + width: "110px", + [theme.breakpoints.down("sm")]: { + width: "auto", + flex: "1 0 0", + }, +} + +const formatStyle = { + flex: "1 0 0", +} + +const DateLabel = styled(DifferingRunLabel)(dateColumnStyle) + +const PriceLabel = styled(DifferingRunLabel)(priceColumnStyle) + +const FormatLabel = styled(DifferingRunLabel)(formatStyle) + +const DateData = styled(DifferingRunData)(dateColumnStyle) + +const PriceData = styled(DifferingRunData)(priceColumnStyle) + +const FormatData = styled(DifferingRunData)(formatStyle) + const DifferingRunLocation = styled(DifferingRunData)({ flex: "1 0 100%", flexDirection: "column", @@ -63,62 +92,27 @@ const DifferingRunLocation = styled(DifferingRunData)({ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({ resource, }) => { - if (!resource.runs) { - return null - } - if (resource.runs.length === 1) { - return null - } const asTaughtIn = resource ? showStartAnytime(resource) : false - const prices: LearningResourcePrice[] = [] - const deliveryMethods = [] - const locations = [] - for (const run of resource.runs) { - if (run.resource_prices) { - run.resource_prices.forEach((price) => { - if (price.amount !== "0") { - prices.push(price) - } - }) - } - if (run.delivery) { - deliveryMethods.push(run.delivery) - } - if (run.location) { - locations.push(run.location) - } - } - const distinctPrices = [...new Set(prices.map((p) => p.amount).flat())] - const distinctDeliveryMethods = [ - ...new Set(deliveryMethods.flat().map((dm) => dm?.code)), - ] - const distinctLocations = [...new Set(locations.flat().map((l) => l))] - if ( - distinctPrices.length > 1 || - distinctDeliveryMethods.length > 1 || - distinctLocations.length > 1 - ) { + if (!allRunsAreIdentical(resource)) { return ( - Date - Price - Format + Date + Price + Format - {resource.runs.map((run, index) => ( + {resource.runs?.map((run, index) => ( - - {formatRunDate(run, asTaughtIn)} - + {formatRunDate(run, asTaughtIn)} {run.resource_prices && ( - + {getDisplayPrice(getRunPrices(run)["course"])} - + )} {run.delivery && ( - + {run.delivery?.map((dm) => dm?.name).join(", ")} - + )} {run.delivery.filter((d) => d.code === "in_person").length > 0 && run.location && ( diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx index d14bac2176..2ae5b4a370 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx @@ -5,6 +5,7 @@ import InfoSectionV2 from "./InfoSectionV2" import { ThemeProvider } from "../ThemeProvider/ThemeProvider" import { formatRunDate } from "ol-utilities" import invariant from "tiny-invariant" +import user from "@testing-library/user-event" // This is a pipe followed by a zero-width space const SEPARATOR = "|​" @@ -134,9 +135,9 @@ describe("Learning resource info section start date", () => { within(section).getByText(runDate) }) - test("Multiple Runs", () => { - const course = courses.free.multipleRuns - const expectedDateText = course.runs + test("Multiple run dates", () => { + const course = courses.multipleRuns.sameData + const expectedDateText = `${course.runs ?.sort((a, b) => { if (a?.start_date && b?.start_date) { return Date.parse(a.start_date) - Date.parse(b.start_date) @@ -144,15 +145,39 @@ describe("Learning resource info section start date", () => { return 0 }) .map((run) => formatRunDate(run, false)) - .join(SEPARATOR) + .slice(0, 2) + .join(SEPARATOR)}Show more` invariant(expectedDateText) render(, { wrapper: ThemeProvider, }) const section = screen.getByTestId("drawer-info-items") - within(section).getByText((_content, node) => { + within(section).getAllByText((_content, node) => { return node?.textContent === expectedDateText || false }) }) + + test("If data is different, dates are not shown", () => { + const course = courses.multipleRuns.differentData + render(, { + wrapper: ThemeProvider, + }) + const section = screen.getByTestId("drawer-info-items") + expect(within(section).queryByText("Start Date:")).toBeNull() + }) + + test("Clicking the show more button should show more dates", async () => { + const course = courses.multipleRuns.sameData + const totalRuns = course.runs?.length ? course.runs.length : 0 + render(, { + wrapper: ThemeProvider, + }) + + const runDates = screen.getByTestId("drawer-run-dates") + expect(runDates.children.length).toBe(3) + const showMoreLink = within(runDates).getByText("Show more") + await user.click(showMoreLink) + expect(runDates.children.length).toBe(totalRuns + 1) + }) }) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index d0464760bb..70615e470b 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useState } from "react" import styled from "@emotion/styled" import ISO6391 from "iso-639-1" import { @@ -16,6 +16,7 @@ import { } from "@remixicon/react" import { LearningResource, ResourceTypeEnum } from "api" import { + allRunsAreIdentical, formatDurationClockTime, formatRunDate, getLearningResourcePrices, @@ -23,6 +24,7 @@ import { } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" import DifferingRunsTable from "./DifferingRunsTable" +import { Link } from "../Link/Link" const SeparatorContainer = styled.span({ padding: "0 8px", @@ -87,6 +89,19 @@ const InfoValue = styled.div({ ...theme.typography.body3, }) +const NoWrap = styled.span({ + whiteSpace: "nowrap", +}) + +const ShowMoreLink = styled(Link)({ + paddingLeft: "12px", +}) + +const ShowLessLink = styled(Link)({ + display: "flex", + paddingTop: "4px", +}) + const PriceDisplay = styled.div({ display: "flex", alignItems: "center", @@ -145,6 +160,77 @@ const InfoItemValue: React.FC = ({ ) } +const RunDates: React.FC<{ resource: LearningResource }> = ({ resource }) => { + const [showingMore, setShowingMore] = useState(false) + const asTaughtIn = showStartAnytime(resource) + const sortedDates = resource.runs + ?.sort((a, b) => { + if (a?.start_date && b?.start_date) { + return Date.parse(a.start_date) - Date.parse(b.start_date) + } + return 0 + }) + .map((run) => formatRunDate(run, asTaughtIn)) + const totalDates = sortedDates?.length || 0 + const showMore = totalDates > 2 + if (showMore) { + const ShowHideLink = showingMore ? ShowLessLink : ShowMoreLink + const showMoreLink = ( + + setShowingMore(!showingMore)} + > + {showingMore ? "Show less" : "Show more"} + + + ) + return ( + + {sortedDates?.slice(0, 2).map((runDate, index) => { + return ( + + + + ) + })} + {!showingMore && showMoreLink} + {showingMore && + sortedDates?.slice(2).map((runDate, index) => { + return ( + + + + ) + })} + {showingMore && showMoreLink} + + ) + } else { + const runDates = sortedDates?.map((runDate, index) => { + return ( + + + + ) + }) + return {runDates} + } +} + const INFO_ITEMS: InfoItemConfig = [ { label: (resource: LearningResource) => { @@ -154,33 +240,10 @@ const INFO_ITEMS: InfoItemConfig = [ }, Icon: RiCalendarLine, selector: (resource: LearningResource) => { - const asTaughtIn = resource ? showStartAnytime(resource) : false - if ( - [ResourceTypeEnum.Course, ResourceTypeEnum.Program].includes( - resource.resource_type as "course" | "program", - ) - ) { - const sortedDates = - resource.runs - ?.sort((a, b) => { - if (a?.start_date && b?.start_date) { - return Date.parse(a.start_date) - Date.parse(b.start_date) - } - return 0 - }) - .map((run) => formatRunDate(run, asTaughtIn)) ?? [] - const runDates = - sortedDates.map((runDate, index) => { - return ( - - ) - }) ?? [] - return runDates + const totalDatesWithRuns = + resource.runs?.filter((run) => run.start_date !== null).length || 0 + if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) { + return } else return null }, }, @@ -396,9 +459,11 @@ const InfoSectionV2 = ({ resource }: { resource?: LearningResource }) => { <> - {infoItems.map((props, index) => ( - - ))} + {infoItems + .filter((props) => props.value !== null) + .map((props, index) => ( + + ))} ) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx index f05fc4e9a6..4fdefd396e 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV2.tsx @@ -78,19 +78,19 @@ const RightContainer = styled.div({ }, }) -const ImageContainer = styled.div<{ aspect: number }>` - position: relative; - width: 100%; - padding-bottom: ${({ aspect }) => 100 / aspect}%; -` - -const Image = styled(NextImage)({ - borderRadius: "8px", +const ImageContainer = styled.div({ width: "100%", - objectFit: "cover", - zIndex: -1, }) +const Image = styled(NextImage)<{ aspect: number }>` + position: relative !important; + border-radius: 8px; + width: 100%; + aspect-ratio: ${({ aspect }) => aspect}; + object-fit: cover; + z-index: -1; +` + const SkeletonImage = styled(Skeleton)<{ aspect: number }>((aspect) => ({ borderRadius: "8px", paddingBottom: `${100 / aspect.aspect}%`, @@ -244,20 +244,22 @@ const ImageSection: React.FC<{ ) } else if (resource?.image) { return ( - + {resource?.image.alt ) } else if (resource) { return ( - + {resource.image?.alt diff --git a/frontends/ol-utilities/src/learning-resources/learning-resources.test.ts b/frontends/ol-utilities/src/learning-resources/learning-resources.test.ts index 6d36d2d010..fae76e8f34 100644 --- a/frontends/ol-utilities/src/learning-resources/learning-resources.test.ts +++ b/frontends/ol-utilities/src/learning-resources/learning-resources.test.ts @@ -1,6 +1,7 @@ -import { findBestRun } from "./learning-resources" +import { allRunsAreIdentical, findBestRun } from "./learning-resources" import * as factories from "api/test-utils/factories" import { faker } from "@faker-js/faker/locale/en" +import { CourseResourceDeliveryInnerCodeEnum } from "api" const makeRun = factories.learningResources.run const fromNow = (days: number): string => { @@ -80,3 +81,127 @@ describe("findBestRun", () => { expect(actual).toEqual(expected) }) }) + +describe("allRunsAreIdentical", () => { + test("returns true if no runs", () => { + const resource = factories.learningResources.resource() + resource.runs = [] + expect(allRunsAreIdentical(resource)).toBe(true) + }) + + test("returns true if only one run", () => { + const resource = factories.learningResources.resource() + resource.runs = [makeRun()] + expect(allRunsAreIdentical(resource)).toBe(true) + }) + + test("returns true if all runs are identical", () => { + const resource = factories.learningResources.resource() + const prices = [{ amount: "100", currency: "USD" }] + const delivery = [ + { code: CourseResourceDeliveryInnerCodeEnum.InPerson, name: "In person" }, + ] + const location = "New York" + resource.runs = [ + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + ] + expect(allRunsAreIdentical(resource)).toBe(true) + }) + + test("returns false if prices differ", () => { + const resource = factories.learningResources.resource() + const prices = [{ amount: "100", currency: "USD" }] + const delivery = [ + { code: CourseResourceDeliveryInnerCodeEnum.InPerson, name: "In person" }, + ] + const location = "New York" + resource.runs = [ + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: [{ amount: "150", currency: "USD" }], + delivery: delivery, + location: location, + }), + ] + expect(allRunsAreIdentical(resource)).toBe(false) + }) + + test("returns false if delivery methods differ", () => { + const resource = factories.learningResources.resource() + const prices = [{ amount: "100", currency: "USD" }] + const delivery = [ + { code: CourseResourceDeliveryInnerCodeEnum.InPerson, name: "In person" }, + ] + const location = "New York" + resource.runs = [ + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: [ + { code: CourseResourceDeliveryInnerCodeEnum.Online, name: "Online" }, + ], + location: location, + }), + ] + expect(allRunsAreIdentical(resource)).toBe(false) + }) + + test("returns false if locations differ", () => { + const resource = factories.learningResources.resource() + const prices = [{ amount: "100", currency: "USD" }] + const delivery = [ + { code: CourseResourceDeliveryInnerCodeEnum.InPerson, name: "In person" }, + ] + const location = "New York" + resource.runs = [ + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: location, + }), + makeRun({ + resource_prices: prices, + delivery: delivery, + location: "San Francisco", + }), + ] + expect(allRunsAreIdentical(resource)).toBe(false) + }) +}) diff --git a/frontends/ol-utilities/src/learning-resources/learning-resources.ts b/frontends/ol-utilities/src/learning-resources/learning-resources.ts index 5c59d48cab..74e6973ec5 100644 --- a/frontends/ol-utilities/src/learning-resources/learning-resources.ts +++ b/frontends/ol-utilities/src/learning-resources/learning-resources.ts @@ -1,6 +1,6 @@ import moment from "moment" import type { LearningResource, LearningResourceRun } from "api" -import { ResourceTypeEnum } from "api" +import { DeliveryEnum, ResourceTypeEnum } from "api" import { capitalize } from "lodash" import { formatDate } from "../date/format" @@ -116,6 +116,52 @@ const formatRunDate = ( return null } +/** + * Checks if all runs of a given learning resource are identical in terms of price, delivery method, and location. + * + * @param resource - The learning resource to check. + * @returns `true` if all runs have the same price, delivery method, and location; otherwise, `false`. + */ +const allRunsAreIdentical = (resource: LearningResource) => { + if (!resource.runs) { + return true + } + if (resource.runs.length <= 1) { + return true + } + const amounts = new Set() + const currencies = new Set() + const deliveryMethods = new Set() + const locations = new Set() + for (const run of resource.runs) { + if (run.resource_prices) { + run.resource_prices.forEach((price) => { + if (!(resource.free && price.amount === "0")) { + amounts.add(price.amount) + currencies.add(price.currency) + } + }) + } + if (run.delivery) { + for (const dm of run.delivery) { + deliveryMethods.add(dm.code) + } + } + if (run.location) { + locations.add(run.location) + } + } + const hasInPerson = [...deliveryMethods].some( + (dm) => dm === DeliveryEnum.InPerson, + ) + return ( + amounts.size === 1 && + currencies.size === 1 && + deliveryMethods.size === 1 && + (hasInPerson ? locations.size === 1 : locations.size === 0) + ) +} + export { DEFAULT_RESOURCE_IMG, embedlyCroppedImage, @@ -123,5 +169,6 @@ export { getReadableResourceType, findBestRun, formatRunDate, + allRunsAreIdentical, } export type { EmbedlyConfig } From 1a2b98e2703b3e57de465a00ee079da1ba361926 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:58:27 +0100 Subject: [PATCH 10/16] Mechanism to sync server prefetch with client API calls (#1798) * Warn on API calls during initial render not prefetched * Full prefetch for homepage (commented) * Prefetch utility * Check for queries prefetched that are not needed during render and warn * No need to stringify * Replace useQuery overrides with decoupled cache check (wip) * Observer count for unnecessary prefetch warnings * Remove useQuery override * Test prefetch warnings * Remove inadvertent/unnecessary diff * Remove comments * Remove comment * Update frontends/api/src/ssr/usePrefetchWarnings.test.ts Co-authored-by: Chris Chudzicki * Remove comment as no longer true * Less specific object assertion --------- Co-authored-by: Chris Chudzicki --- frontends/api/package.json | 1 + frontends/api/src/hooks/channels/index.ts | 2 + .../api/src/hooks/learningResources/index.ts | 8 +- frontends/api/src/hooks/newsEvents/index.ts | 7 +- frontends/api/src/hooks/testimonials/index.ts | 6 +- frontends/api/src/hooks/widget_lists/index.ts | 3 +- frontends/api/src/ssr/prefetch.ts | 13 ++ .../api/src/ssr/usePrefetchWarnings.test.ts | 118 ++++++++++++++++++ frontends/api/src/ssr/usePrefetchWarnings.ts | 96 ++++++++++++++ .../main/src/app-pages/HomePage/HomePage.tsx | 26 ++-- frontends/main/src/app/departments/page.tsx | 20 ++- frontends/main/src/app/page.tsx | 27 +++- frontends/main/src/app/providers.tsx | 3 + 13 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 frontends/api/src/ssr/prefetch.ts create mode 100644 frontends/api/src/ssr/usePrefetchWarnings.test.ts create mode 100644 frontends/api/src/ssr/usePrefetchWarnings.ts diff --git a/frontends/api/package.json b/frontends/api/package.json index ecaa7c61c2..12acd63404 100644 --- a/frontends/api/package.json +++ b/frontends/api/package.json @@ -10,6 +10,7 @@ "./v1": "./src/generated/v1/api.ts", "./hooks/*": "./src/hooks/*/index.ts", "./constants": "./src/common/constants.ts", + "./ssr/*": "./src/ssr/*.ts", "./test-utils/factories": "./src/test-utils/factories/index.ts", "./test-utils": "./src/test-utils/index.ts" }, diff --git a/frontends/api/src/hooks/channels/index.ts b/frontends/api/src/hooks/channels/index.ts index b86f798c58..549e77778b 100644 --- a/frontends/api/src/hooks/channels/index.ts +++ b/frontends/api/src/hooks/channels/index.ts @@ -27,6 +27,7 @@ const useChannelDetail = (channelType: string, channelName: string) => { ...channels.detailByType(channelType, channelName), }) } + const useChannelCounts = (channelType: string) => { return useQuery({ ...channels.countsByType(channelType), @@ -54,4 +55,5 @@ export { useChannelsList, useChannelPartialUpdate, useChannelCounts, + channels as channelsKeyFactory, } diff --git a/frontends/api/src/hooks/learningResources/index.ts b/frontends/api/src/hooks/learningResources/index.ts index 7633c806f8..6c2ea60ef3 100644 --- a/frontends/api/src/hooks/learningResources/index.ts +++ b/frontends/api/src/hooks/learningResources/index.ts @@ -500,13 +500,6 @@ const useSchoolsList = () => { return useQuery(learningResources.schools()) } -/* - * Not intended to be imported except for special cases. - * It's used in the ResourceCarousel to dynamically build a single useQueries hook - * from config because a React component cannot conditionally call hooks during renders. - */ -export { default as learningResourcesKeyFactory } from "./keyFactory" - export { useLearningResourcesList, useFeaturedLearningResourcesList, @@ -538,4 +531,5 @@ export { useListItemMove, usePlatformsList, useSchoolsList, + learningResources as learningResourcesKeyFactory, } diff --git a/frontends/api/src/hooks/newsEvents/index.ts b/frontends/api/src/hooks/newsEvents/index.ts index 0e14708dba..b6f0d9b26f 100644 --- a/frontends/api/src/hooks/newsEvents/index.ts +++ b/frontends/api/src/hooks/newsEvents/index.ts @@ -15,4 +15,9 @@ const useNewsEventsDetail = (id: number) => { return useQuery(newsEvents.detail(id)) } -export { useNewsEventsList, useNewsEventsDetail, NewsEventsListFeedTypeEnum } +export { + useNewsEventsList, + useNewsEventsDetail, + NewsEventsListFeedTypeEnum, + newsEvents as newsEventsKeyFactory, +} diff --git a/frontends/api/src/hooks/testimonials/index.ts b/frontends/api/src/hooks/testimonials/index.ts index 2ee1c6f4c5..da98b49afc 100644 --- a/frontends/api/src/hooks/testimonials/index.ts +++ b/frontends/api/src/hooks/testimonials/index.ts @@ -23,4 +23,8 @@ const useTestimonialDetail = (id: number | undefined) => { }) } -export { useTestimonialDetail, useTestimonialList } +export { + useTestimonialDetail, + useTestimonialList, + testimonials as testimonialsKeyFactory, +} diff --git a/frontends/api/src/hooks/widget_lists/index.ts b/frontends/api/src/hooks/widget_lists/index.ts index 2ac9aff1f4..1c8c2c42d2 100644 --- a/frontends/api/src/hooks/widget_lists/index.ts +++ b/frontends/api/src/hooks/widget_lists/index.ts @@ -3,8 +3,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { widgetListsApi } from "../../clients" import widgetLists from "./keyFactory" import { WidgetInstance } from "api/v0" + /** - * Query is diabled if id is undefined. + * Query is disabled if id is undefined. */ const useWidgetList = (id: number | undefined) => { return useQuery({ diff --git a/frontends/api/src/ssr/prefetch.ts b/frontends/api/src/ssr/prefetch.ts new file mode 100644 index 0000000000..6078fcafec --- /dev/null +++ b/frontends/api/src/ssr/prefetch.ts @@ -0,0 +1,13 @@ +import { QueryClient, dehydrate } from "@tanstack/react-query" +import type { Query } from "@tanstack/react-query" + +// Utility to avoid repetition in server components +export const prefetch = async (queries: (Query | unknown)[]) => { + const queryClient = new QueryClient() + + await Promise.all( + queries.map((query) => queryClient.prefetchQuery(query as Query)), + ) + + return dehydrate(queryClient) +} diff --git a/frontends/api/src/ssr/usePrefetchWarnings.test.ts b/frontends/api/src/ssr/usePrefetchWarnings.test.ts new file mode 100644 index 0000000000..ab34473ba8 --- /dev/null +++ b/frontends/api/src/ssr/usePrefetchWarnings.test.ts @@ -0,0 +1,118 @@ +import { renderHook } from "@testing-library/react" +import { useQuery } from "@tanstack/react-query" +import { usePrefetchWarnings } from "./usePrefetchWarnings" +import { setupReactQueryTest } from "../hooks/test-utils" +import { urls, factories, setMockResponse } from "../test-utils" +import { + learningResourcesKeyFactory, + useLearningResourcesDetail, +} from "../hooks/learningResources" + +jest.mock("./usePrefetchWarnings", () => { + const originalModule = jest.requireActual("./usePrefetchWarnings") + return { + ...originalModule, + logQueries: jest.fn(), + } +}) + +describe("SSR prefetch warnings", () => { + beforeEach(() => { + jest.spyOn(console, "info").mockImplementation(() => {}) + jest.spyOn(console, "table").mockImplementation(() => {}) + }) + + it("Warns if a query is requested on the client that has not been prefetched", async () => { + const { wrapper, queryClient } = setupReactQueryTest() + + const data = factories.learningResources.resource() + setMockResponse.get(urls.learningResources.details({ id: 1 }), data) + + renderHook(() => useLearningResourcesDetail(1), { wrapper }) + + renderHook(usePrefetchWarnings, { + wrapper, + initialProps: { queryClient }, + }) + + expect(console.info).toHaveBeenCalledWith( + "The following queries were requested in first render but not prefetched.", + "If these queries are user-specific, they cannot be prefetched - responses are cached on public CDN.", + "Otherwise, consider fetching on the server with prefetch:", + ) + expect(console.table).toHaveBeenCalledWith( + [ + expect.objectContaining({ + disabled: false, + initialStatus: "loading", + key: learningResourcesKeyFactory.detail(1).queryKey, + observerCount: 1, + }), + ], + ["hash", "initialStatus", "status", "observerCount", "disabled"], + ) + }) + + it("Ignores exempted queries requested on the client that have not been prefetched", async () => { + const { wrapper, queryClient } = setupReactQueryTest() + + const data = factories.learningResources.resource() + setMockResponse.get(urls.learningResources.details({ id: 1 }), data) + + renderHook(() => useLearningResourcesDetail(1), { wrapper }) + + renderHook(usePrefetchWarnings, { + wrapper, + initialProps: { + queryClient, + exemptions: [learningResourcesKeyFactory.detail(1).queryKey], + }, + }) + + expect(console.info).not.toHaveBeenCalled() + expect(console.table).not.toHaveBeenCalled() + }) + + it("Warns for queries prefetched on the server but not requested on the client", async () => { + const { wrapper, queryClient } = setupReactQueryTest() + + const data = factories.learningResources.resource() + setMockResponse.get(urls.learningResources.details({ id: 1 }), data) + + // Emulate server prefetch + const { unmount } = renderHook( + () => + useQuery({ + ...learningResourcesKeyFactory.detail(1), + initialData: data, + }), + { wrapper }, + ) + + // Removes observer + unmount() + + renderHook(usePrefetchWarnings, { + wrapper, + initialProps: { queryClient }, + }) + + expect(console.info).toHaveBeenCalledWith( + "The following queries were prefetched on the server but not accessed during initial render.", + "If these queries are no longer in use they should removed from prefetch:", + ) + expect(console.table).toHaveBeenCalledWith( + [ + { + disabled: false, + hash: JSON.stringify(learningResourcesKeyFactory.detail(1).queryKey), + initialStatus: "success", + key: learningResourcesKeyFactory.detail(1).queryKey, + observerCount: 0, + status: "success", + }, + ], + ["hash", "initialStatus", "status", "observerCount", "disabled"], + ) + }) +}) diff --git a/frontends/api/src/ssr/usePrefetchWarnings.ts b/frontends/api/src/ssr/usePrefetchWarnings.ts new file mode 100644 index 0000000000..000d80261c --- /dev/null +++ b/frontends/api/src/ssr/usePrefetchWarnings.ts @@ -0,0 +1,96 @@ +import { useEffect } from "react" +import type { Query, QueryClient, QueryKey } from "@tanstack/react-query" + +const logQueries = (...args: [...string[], Query[]]) => { + const queries = args.pop() as Query[] + console.info(...args) + console.table( + queries.map((query) => ({ + key: query.queryKey, + hash: query.queryHash, + disabled: query.isDisabled(), + initialStatus: query.initialState.status, + status: query.state.status, + observerCount: query.getObserversCount(), + })), + ["hash", "initialStatus", "status", "observerCount", "disabled"], + ) +} + +const PREFETCH_EXEMPT_QUERIES = [["userMe"]] + +/** + * Call this as high as possible in render tree to detect query usage on + * first render. + */ +export const usePrefetchWarnings = ({ + queryClient, + exemptions = [], +}: { + queryClient: QueryClient + /** + * A list of query keys that should be exempted. + * + * NOTE: This uses react-query's hierarchical key matching, so exempting + * ["a", { x: 1 }] will exempt + * - ["a", { x: 1 }] + * - ["a", { x: 1, y: 2 }] + * - ["a", { x: 1, y: 2 }, ...any_other_entries] + */ + exemptions?: QueryKey[] +}) => { + /** + * NOTE: React renders components top-down, but effects run bottom-up, so + * this effect will run after all child effects. + */ + useEffect( + () => { + if (process.env.NODE_ENV === "production") { + return + } + + const cache = queryClient.getQueryCache() + const queries = cache.getAll() + + const exempted = [...exemptions, ...PREFETCH_EXEMPT_QUERIES].map((key) => + cache.find(key), + ) + + const potentialPrefetches = queries.filter( + (query) => + !exempted.includes(query) && + query.initialState.status !== "success" && + !query.isDisabled(), + ) + + if (potentialPrefetches.length > 0) { + logQueries( + "The following queries were requested in first render but not prefetched.", + "If these queries are user-specific, they cannot be prefetched - responses are cached on public CDN.", + "Otherwise, consider fetching on the server with prefetch:", + potentialPrefetches, + ) + } + + const unusedPrefetches = queries.filter( + (query) => + !exempted.includes(query) && + query.initialState.status === "success" && + query.getObserversCount() === 0 && + !query.isDisabled(), + ) + + if (unusedPrefetches.length > 0) { + logQueries( + "The following queries were prefetched on the server but not accessed during initial render.", + "If these queries are no longer in use they should removed from prefetch:", + unusedPrefetches, + ) + } + }, + // We only want to run this on initial render. + // (Aside: queryClient should be a singleton anyway) + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) +} diff --git a/frontends/main/src/app-pages/HomePage/HomePage.tsx b/frontends/main/src/app-pages/HomePage/HomePage.tsx index 475e53efd7..ac4e7891a0 100644 --- a/frontends/main/src/app-pages/HomePage/HomePage.tsx +++ b/frontends/main/src/app-pages/HomePage/HomePage.tsx @@ -1,6 +1,6 @@ "use client" -import React, { Suspense } from "react" +import React from "react" import { Container, styled, theme } from "ol-components" import HeroSearch from "@/page-components/HeroSearch/HeroSearch" import BrowseTopicsSection from "./BrowseTopicsSection" @@ -52,25 +52,21 @@ const HomePage: React.FC = () => {
- - - +
- - - + diff --git a/frontends/main/src/app/departments/page.tsx b/frontends/main/src/app/departments/page.tsx index 41364044ee..a9b5133e9f 100644 --- a/frontends/main/src/app/departments/page.tsx +++ b/frontends/main/src/app/departments/page.tsx @@ -1,15 +1,27 @@ import React from "react" import { Metadata } from "next" - +import DepartmentListingPage from "@/app-pages/DepartmentListingPage/DepartmentListingPage" import { standardizeMetadata } from "@/common/metadata" +import { Hydrate } from "@tanstack/react-query" +import { learningResourcesKeyFactory } from "api/hooks/learningResources" +import { channelsKeyFactory } from "api/hooks/channels" +import { prefetch } from "api/ssr/prefetch" + export const metadata: Metadata = standardizeMetadata({ title: "Departments", }) -import DepartmentListingPage from "@/app-pages/DepartmentListingPage/DepartmentListingPage" +const Page: React.FC = async () => { + const dehydratedState = await prefetch([ + channelsKeyFactory.countsByType("department"), + learningResourcesKeyFactory.schools(), + ]) -const Page: React.FC = () => { - return + return ( + + + + ) } export default Page diff --git a/frontends/main/src/app/page.tsx b/frontends/main/src/app/page.tsx index 34dd27b718..ae920e6784 100644 --- a/frontends/main/src/app/page.tsx +++ b/frontends/main/src/app/page.tsx @@ -2,6 +2,13 @@ import React from "react" import type { Metadata } from "next" import HomePage from "@/app-pages/HomePage/HomePage" import { getMetadataAsync } from "@/common/metadata" +import { Hydrate } from "@tanstack/react-query" +import { testimonialsKeyFactory } from "api/hooks/testimonials" +import { + NewsEventsListFeedTypeEnum, + newsEventsKeyFactory, +} from "api/hooks/newsEvents" +import { prefetch } from "api/ssr/prefetch" type SearchParams = { [key: string]: string | string[] | undefined @@ -19,7 +26,25 @@ export async function generateMetadata({ } const Page: React.FC = async () => { - return + const dehydratedState = await prefetch([ + testimonialsKeyFactory.list({ position: 1 }), + newsEventsKeyFactory.list({ + feed_type: [NewsEventsListFeedTypeEnum.News], + limit: 6, + sortby: "-news_date", + }), + newsEventsKeyFactory.list({ + feed_type: [NewsEventsListFeedTypeEnum.Events], + limit: 5, + sortby: "event_date", + }), + ]) + + return ( + + + + ) } export default Page diff --git a/frontends/main/src/app/providers.tsx b/frontends/main/src/app/providers.tsx index 7c0506a702..ddd3a29456 100644 --- a/frontends/main/src/app/providers.tsx +++ b/frontends/main/src/app/providers.tsx @@ -6,10 +6,13 @@ import { QueryClientProvider } from "@tanstack/react-query" import { ThemeProvider, NextJsAppRouterCacheProvider } from "ol-components" import { Provider as NiceModalProvider } from "@ebay/nice-modal-react" import ConfiguredPostHogProvider from "@/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider" +import { usePrefetchWarnings } from "api/ssr/usePrefetchWarnings" export default function Providers({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient() + usePrefetchWarnings({ queryClient }) + return ( From b95a18400fe14d67f6ca14eae48ae1fa81d8e71e Mon Sep 17 00:00:00 2001 From: Carey P Gumaer Date: Fri, 15 Nov 2024 11:48:00 -0500 Subject: [PATCH 11/16] v2 drawer certification updates (#1823) * update certification display in v2 drawer to match latest designs * don't show price info item if runs have differing data * MicroMasters not Micromasters * if there is no price for the certificate but it's indicated that one is included, display that * if resource is free, includes a certification but has no prices, still display the pill in the info item * generate migration for MicroMasters spelling change * fix certificate pill padding on mobile --- frontends/api/src/generated/v1/api.ts | 98 +++++++++---------- .../LearningResourceCard/testUtils.ts | 20 ++++ .../InfoSectionV2.test.tsx | 16 ++- .../InfoSectionV2.tsx | 55 ++++++++--- learning_resources/constants.py | 2 +- ...ter_learningresource_certification_type.py | 26 +++++ openapi/specs/v1.yaml | 44 ++++----- 7 files changed, 169 insertions(+), 92 deletions(-) create mode 100644 learning_resources/migrations/0076_alter_learningresource_certification_type.py diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index c49bbf18d3..3310762142 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -189,13 +189,13 @@ export type AvailabilityEnum = (typeof AvailabilityEnum)[keyof typeof AvailabilityEnum] /** - * * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @export * @enum {string} */ export const CertificationTypeEnumDescriptions = { - micromasters: "Micromasters Credential", + micromasters: "MicroMasters Credential", professional: "Professional Certificate", completion: "Certificate of Completion", none: "No Certificate", @@ -203,7 +203,7 @@ export const CertificationTypeEnumDescriptions = { export const CertificationTypeEnum = { /** - * Micromasters Credential + * MicroMasters Credential */ Micromasters: "micromasters", /** @@ -3853,7 +3853,7 @@ export interface PercolateQuerySubscriptionRequestRequest { */ certification?: boolean | null /** - * The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array} * @memberof PercolateQuerySubscriptionRequestRequest */ @@ -8973,7 +8973,7 @@ export const CoursesApiAxiosParamCreator = function ( * Get a paginated list of courses * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -9260,7 +9260,7 @@ export const CoursesApiFp = function (configuration?: Configuration) { * Get a paginated list of courses * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -9571,7 +9571,7 @@ export interface CoursesApiCoursesListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof CoursesApiCoursesList */ @@ -10334,7 +10334,7 @@ export const FeaturedApiAxiosParamCreator = function ( * Get a paginated list of featured resources * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -10535,7 +10535,7 @@ export const FeaturedApiFp = function (configuration?: Configuration) { * Get a paginated list of featured resources * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -10719,7 +10719,7 @@ export interface FeaturedApiFeaturedListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof FeaturedApiFeaturedList */ @@ -11423,7 +11423,7 @@ export const LearningResourcesApiAxiosParamCreator = function ( * Get a paginated list of learning resources. * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -11615,7 +11615,7 @@ export const LearningResourcesApiAxiosParamCreator = function ( * @summary Get similar resources * @param {number} id * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -11828,7 +11828,7 @@ export const LearningResourcesApiAxiosParamCreator = function ( * @summary Get similar resources using vector embeddings * @param {number} id * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -12200,7 +12200,7 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { * Get a paginated list of learning resources. * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -12314,7 +12314,7 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { * @summary Get similar resources * @param {number} id * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -12439,7 +12439,7 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { * @summary Get similar resources using vector embeddings * @param {number} id * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -12961,7 +12961,7 @@ export interface LearningResourcesApiLearningResourcesListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof LearningResourcesApiLearningResourcesList */ @@ -13108,7 +13108,7 @@ export interface LearningResourcesApiLearningResourcesSimilarListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ @@ -13269,7 +13269,7 @@ export interface LearningResourcesApiLearningResourcesVectorSimilarListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ @@ -14195,7 +14195,7 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( * @summary Search * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -14411,7 +14411,7 @@ export const LearningResourcesSearchApiFp = function ( * @summary Search * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -14600,7 +14600,7 @@ export interface LearningResourcesSearchApiLearningResourcesSearchRetrieveReques readonly certification?: boolean | null /** - * The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesSearchApiLearningResourcesSearchRetrieve */ @@ -15037,7 +15037,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @summary Check if a user is subscribed to a specific query * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -15246,7 +15246,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @summary List subscribed queries * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -15449,7 +15449,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @summary Subscribe user to query * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -15731,7 +15731,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @summary Check if a user is subscribed to a specific query * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -15846,7 +15846,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @summary List subscribed queries * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -15958,7 +15958,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @summary Subscribe user to query * @param {Array} [aggregations] Show resource counts by category * @param {boolean | null} [certification] True if the learning resource offers a certificate - * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {number | null} [content_file_score_weight] Score weight for content file data. 1 is the default. 0 means content files are ignored * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline @@ -16290,7 +16290,7 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr readonly certification?: boolean | null /** - * The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionCheckList */ @@ -16493,7 +16493,7 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr readonly certification?: boolean | null /** - * The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionList */ @@ -16689,7 +16689,7 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr readonly certification?: boolean | null /** - * The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionSubscribeCreate */ @@ -18092,7 +18092,7 @@ export const LearningpathsApiAxiosParamCreator = function ( * Get a paginated list of learning paths * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -18635,7 +18635,7 @@ export const LearningpathsApiFp = function (configuration?: Configuration) { * Get a paginated list of learning paths * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -19203,7 +19203,7 @@ export interface LearningpathsApiLearningpathsListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof LearningpathsApiLearningpathsList */ @@ -20377,7 +20377,7 @@ export const PodcastEpisodesApiAxiosParamCreator = function ( * Get a paginated list of podcast episodes * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -20579,7 +20579,7 @@ export const PodcastEpisodesApiFp = function (configuration?: Configuration) { * Get a paginated list of podcast episodes * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -20767,7 +20767,7 @@ export interface PodcastEpisodesApiPodcastEpisodesListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof PodcastEpisodesApiPodcastEpisodesList */ @@ -21255,7 +21255,7 @@ export const PodcastsApiAxiosParamCreator = function ( * Get a paginated list of podcasts * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -21532,7 +21532,7 @@ export const PodcastsApiFp = function (configuration?: Configuration) { * Get a paginated list of podcasts * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -21812,7 +21812,7 @@ export interface PodcastsApiPodcastsListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof PodcastsApiPodcastsList */ @@ -22378,7 +22378,7 @@ export const ProgramsApiAxiosParamCreator = function ( * Get a paginated list of programs * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -22579,7 +22579,7 @@ export const ProgramsApiFp = function (configuration?: Configuration) { * Get a paginated list of programs * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -22763,7 +22763,7 @@ export interface ProgramsApiProgramsListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof ProgramsApiProgramsList */ @@ -25490,7 +25490,7 @@ export const VideoPlaylistsApiAxiosParamCreator = function ( * Get a paginated list of video playlists * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -25771,7 +25771,7 @@ export const VideoPlaylistsApiFp = function (configuration?: Configuration) { * Get a paginated list of video playlists * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -26053,7 +26053,7 @@ export interface VideoPlaylistsApiVideoPlaylistsListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof VideoPlaylistsApiVideoPlaylistsList */ @@ -26460,7 +26460,7 @@ export const VideosApiAxiosParamCreator = function ( * Get a paginated list of videos * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -26661,7 +26661,7 @@ export const VideosApiFp = function (configuration?: Configuration) { * Get a paginated list of videos * @summary List * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] Multiple values may be separated by commas. * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies @@ -26844,7 +26844,7 @@ export interface VideosApiVideosListRequest { readonly certification?: boolean /** - * The type of certification offered * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} * @memberof VideosApiVideosList */ diff --git a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts index c1dfaf11c1..70251fbd00 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts +++ b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts @@ -67,6 +67,10 @@ const courses = { runs: [factories.learningResources.run()], free: true, certification: true, + certification_type: { + code: "completion", + name: "Certificate of Completion", + }, resource_prices: [ { amount: "0", currency: "USD" }, { amount: "49", currency: "USD" }, @@ -77,6 +81,10 @@ const courses = { runs: [factories.learningResources.run()], free: true, certification: true, + certification_type: { + code: "completion", + name: "Certificate of Completion", + }, resource_prices: [ { amount: "0", currency: "USD" }, { amount: "99", currency: "USD" }, @@ -125,6 +133,10 @@ const courses = { runs: [factories.learningResources.run()], free: false, certification: true, + certification_type: { + code: "completion", + name: "Certificate of Completion", + }, resource_prices: [], }), }, @@ -141,6 +153,10 @@ const courses = { runs: [factories.learningResources.run()], free: false, certification: true, + certification_type: { + code: "completion", + name: "Certificate of Completion", + }, resource_prices: [{ amount: "49", currency: "USD" }], }), withCertificatePriceRange: makeResource({ @@ -148,6 +164,10 @@ const courses = { runs: [factories.learningResources.run()], free: false, certification: true, + certification_type: { + code: "completion", + name: "Certificate of Completion", + }, resource_prices: [ { amount: "49", currency: "USD" }, { amount: "99", currency: "USD" }, diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx index 2ae5b4a370..41eaa07b18 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx @@ -65,7 +65,10 @@ describe("Learning resource info section pricing", () => { screen.getByText("Paid") expect(screen.queryByText("Free")).toBeNull() - screen.getByText("Certificate included") + screen.getByText("Certificate:") + screen.getByText( + courses.unknownPrice.withCertificate.certification_type.name, + ) }) test("Paid course, no certificate", () => { @@ -87,7 +90,8 @@ describe("Learning resource info section pricing", () => { screen.getByText("$49") expect(screen.queryByText("Paid")).toBeNull() - screen.getByText("Certificate included") + screen.getByText("Certificate:") + screen.getByText(courses.paid.withCerticateOnePrice.certification_type.name) }) test("Paid course, with certificate, price range", () => { @@ -100,7 +104,10 @@ describe("Learning resource info section pricing", () => { screen.getByText("$49 – $99") expect(screen.queryByText("Paid")).toBeNull() - screen.getByText("Certificate included") + screen.getByText("Certificate:") + screen.getByText( + courses.paid.withCertificatePriceRange.certification_type.name, + ) }) }) @@ -158,13 +165,14 @@ describe("Learning resource info section start date", () => { }) }) - test("If data is different, dates are not shown", () => { + test("If data is different, dates and prices are not shown", () => { const course = courses.multipleRuns.differentData render(, { wrapper: ThemeProvider, }) const section = screen.getByTestId("drawer-info-items") expect(within(section).queryByText("Start Date:")).toBeNull() + expect(within(section).queryByText("Price:")).toBeNull() }) test("Clicking the show more button should show more dates", async () => { diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 70615e470b..08b756dbb5 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -13,6 +13,7 @@ import { RiTranslate2, RiPresentationLine, RiAwardFill, + RiAwardLine, } from "@remixicon/react" import { LearningResource, ResourceTypeEnum } from "api" import { @@ -128,7 +129,7 @@ const Certificate = styled.div({ height: "16px", }, [theme.breakpoints.down("sm")]: { - padding: "1px 2px", + padding: "4px 8px", ...theme.typography.subtitle4, }, }) @@ -251,22 +252,44 @@ const INFO_ITEMS: InfoItemConfig = [ label: "Price:", Icon: RiPriceTag3Line, selector: (resource: LearningResource) => { - const prices = getLearningResourcePrices(resource) + if (allRunsAreIdentical(resource)) { + const prices = getLearningResourcePrices(resource) - return ( - -
{prices.course.display}
- {resource.certification && ( - - - {prices.certificate.display - ? "Earn a certificate:" - : "Certificate included"} - {prices.certificate.display} - - )} -
- ) + return ( + +
{resource.free ? "Free" : prices.course.display}
+ {resource.certification && resource.free ? ( + <> + {prices.certificate.display ? ( + + + Earn a certificate: + {prices.certificate.display} + + ) : ( + + + Certificate + + )} + + ) : null} +
+ ) + } else return null + }, + }, + { + label: "Certificate:", + Icon: RiAwardLine, + selector: (resource: LearningResource) => { + return resource.certification_type && !resource.free ? ( + + ) : null }, }, { diff --git a/learning_resources/constants.py b/learning_resources/constants.py index 2f045612e7..713499c5c8 100644 --- a/learning_resources/constants.py +++ b/learning_resources/constants.py @@ -283,7 +283,7 @@ class LearningResourceDelivery(ExtendedEnum): class CertificationType(ExtendedEnum): """Enum for resource certification types""" - micromasters = "Micromasters Credential" + micromasters = "MicroMasters Credential" professional = "Professional Certificate" completion = "Certificate of Completion" none = "No Certificate" diff --git a/learning_resources/migrations/0076_alter_learningresource_certification_type.py b/learning_resources/migrations/0076_alter_learningresource_certification_type.py new file mode 100644 index 0000000000..67347e3bfe --- /dev/null +++ b/learning_resources/migrations/0076_alter_learningresource_certification_type.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2024-11-13 22:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("learning_resources", "0075_learningresourcerun_instructors"), + ] + + operations = [ + migrations.AlterField( + model_name="learningresource", + name="certification_type", + field=models.CharField( + choices=[ + ("micromasters", "MicroMasters Credential"), + ("professional", "Professional Certificate"), + ("completion", "Certificate of Completion"), + ("none", "No Certificate"), + ], + default="none", + max_length=24, + ), + ), + ] diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 5be827d60f..253db6ce25 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -552,7 +552,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -1163,7 +1163,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -1567,7 +1567,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -1978,7 +1978,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -2371,7 +2371,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -3099,11 +3099,11 @@ paths: - none type: string description: |- - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - description: "The type of certificate \n\n* `micromasters` - Micromasters\ + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ \ Credential\n* `professional` - Professional Certificate\n* `completion`\ \ - Certificate of Completion\n* `none` - No Certificate" - in: query @@ -3597,11 +3597,11 @@ paths: - none type: string description: |- - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - description: "The type of certificate \n\n* `micromasters` - Micromasters\ + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ \ Credential\n* `professional` - Professional Certificate\n* `completion`\ \ - Certificate of Completion\n* `none` - No Certificate" - in: query @@ -4120,11 +4120,11 @@ paths: - none type: string description: |- - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - description: "The type of certificate \n\n* `micromasters` - Micromasters\ + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ \ Credential\n* `professional` - Professional Certificate\n* `completion`\ \ - Certificate of Completion\n* `none` - No Certificate" - in: query @@ -4634,11 +4634,11 @@ paths: - none type: string description: |- - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - description: "The type of certificate \n\n* `micromasters` - Micromasters\ + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ \ Credential\n* `professional` - Professional Certificate\n* `completion`\ \ - Certificate of Completion\n* `none` - No Certificate" - in: query @@ -5122,7 +5122,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -5866,7 +5866,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -6270,7 +6270,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -6761,7 +6761,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -7571,7 +7571,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -8041,7 +8041,7 @@ paths: description: |- The type of certification offered - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate @@ -8515,12 +8515,12 @@ components: - none type: string description: |- - * `micromasters` - Micromasters Credential + * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate x-enum-descriptions: - - Micromasters Credential + - MicroMasters Credential - Professional Certificate - Certificate of Completion - No Certificate @@ -11164,7 +11164,7 @@ components: items: $ref: '#/components/schemas/CertificationTypeEnum' description: "The type of certificate \n\n* `micromasters` -\ - \ Micromasters Credential\n* `professional` - Professional Certificate\n\ + \ MicroMasters Credential\n* `professional` - Professional Certificate\n\ * `completion` - Certificate of Completion\n* `none` - No Certificate" department: type: array From 7f2df1f076d9d081b1df9bf231c8a9ad7342f7e5 Mon Sep 17 00:00:00 2001 From: Carey P Gumaer Date: Fri, 15 Nov 2024 16:50:17 -0500 Subject: [PATCH 12/16] v2 learning resource drawer formats and location (#1826) * add format info item * display location if format is in_person * add tests * also show location for hybrid courses --- .../LearningResourceCard/testUtils.ts | 37 ++++++++++++++ .../InfoSectionV2.test.tsx | 35 +++++++++++++- .../InfoSectionV2.tsx | 48 ++++++++++++++++++- 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts index 70251fbd00..0d52613332 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts +++ b/frontends/ol-components/src/components/LearningResourceCard/testUtils.ts @@ -261,6 +261,43 @@ const courses = { ], }), }, + multipleFormats: makeResource({ + resource_type: ResourceTypeEnum.Course, + location: "Earth", + delivery: [ + { + code: DeliveryEnum.Online, + name: DeliveryEnumDescriptions.online, + }, + { + code: DeliveryEnum.InPerson, + name: DeliveryEnumDescriptions.in_person, + }, + ], + runs: [ + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + ], + }), + singleFormat: makeResource({ + resource_type: ResourceTypeEnum.Course, + delivery: [ + { + code: DeliveryEnum.Online, + name: DeliveryEnumDescriptions.online, + }, + ], + runs: [ + factories.learningResources.run({ + delivery: sameDataRun.delivery, + resource_prices: sameDataRun.resource_prices, + location: sameDataRun.location, + }), + ], + }), } const resourceArgType = { diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx index 41eaa07b18..20adbd1a05 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.test.tsx @@ -165,7 +165,7 @@ describe("Learning resource info section start date", () => { }) }) - test("If data is different, dates and prices are not shown", () => { + test("If data is different then dates, formats, locations and prices are not shown", () => { const course = courses.multipleRuns.differentData render(, { wrapper: ThemeProvider, @@ -173,6 +173,8 @@ describe("Learning resource info section start date", () => { const section = screen.getByTestId("drawer-info-items") expect(within(section).queryByText("Start Date:")).toBeNull() expect(within(section).queryByText("Price:")).toBeNull() + expect(within(section).queryByText("Format:")).toBeNull() + expect(within(section).queryByText("Location:")).toBeNull() }) test("Clicking the show more button should show more dates", async () => { @@ -189,3 +191,34 @@ describe("Learning resource info section start date", () => { expect(runDates.children.length).toBe(totalRuns + 1) }) }) + +describe("Learning resource info section format and location", () => { + test("Multiple formats", () => { + const course = courses.multipleFormats + render(, { + wrapper: ThemeProvider, + }) + + const section = screen.getByTestId("drawer-info-items") + within(section).getAllByText((_content, node) => { + // The pipe in this string is followed by a zero width space + return node?.textContent === "Format:Online|​In person" || false + }) + within(section).getAllByText((_content, node) => { + return node?.textContent === "Location:Earth" || false + }) + }) + + test("Single format", () => { + const course = courses.singleFormat + render(, { + wrapper: ThemeProvider, + }) + + const section = screen.getByTestId("drawer-info-items") + within(section).getAllByText((_content, node) => { + return node?.textContent === "Format:Online" || false + }) + expect(within(section).queryByText("In person")).toBeNull() + }) +}) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 08b756dbb5..6908649c75 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -14,8 +14,10 @@ import { RiPresentationLine, RiAwardFill, RiAwardLine, + RiComputerLine, + RiMapPinLine, } from "@remixicon/react" -import { LearningResource, ResourceTypeEnum } from "api" +import { DeliveryEnum, LearningResource, ResourceTypeEnum } from "api" import { allRunsAreIdentical, formatDurationClockTime, @@ -232,6 +234,15 @@ const RunDates: React.FC<{ resource: LearningResource }> = ({ resource }) => { } } +const shouldShowFormat = (resource: LearningResource) => { + return ( + (resource.resource_type === ResourceTypeEnum.Course || + resource.resource_type === ResourceTypeEnum.Program) && + allRunsAreIdentical(resource) && + resource.delivery + ) +} + const INFO_ITEMS: InfoItemConfig = [ { label: (resource: LearningResource) => { @@ -248,6 +259,41 @@ const INFO_ITEMS: InfoItemConfig = [ } else return null }, }, + { + label: "Format:", + Icon: RiComputerLine, + selector: (resource: LearningResource) => { + if (shouldShowFormat(resource)) { + const totalFormats = resource.delivery?.length || 0 + return resource.delivery.map((format, index) => { + return ( + + ) + }) + } else return null + }, + }, + { + label: "Location:", + Icon: RiMapPinLine, + selector: (resource: LearningResource) => { + if ( + shouldShowFormat(resource) && + resource.delivery?.filter( + (d) => + d.code === DeliveryEnum.InPerson || d.code === DeliveryEnum.Hybrid, + ).length > 0 && + resource.location + ) { + return + } else return null + }, + }, { label: "Price:", Icon: RiPriceTag3Line, From b4ccd6d361a8e54d25eb51ef25e5f4e3c0cad774 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:28:25 +0100 Subject: [PATCH 13/16] LocalDate and NoSSR components to render localized dates only on client --- frontends/main/package.json | 4 ++-- .../app-pages/HomePage/NewsEventsSection.tsx | 20 +++++++++---------- .../LearningResourceCard.tsx | 5 +++-- .../LearningResourceListCard.tsx | 4 ++-- .../DifferingRunsTable.tsx | 5 ++++- .../InfoSectionV2.tsx | 7 ++++++- .../LearningResourceExpandedV1.tsx | 7 ++++--- frontends/ol-utilities/src/date/LocalDate.tsx | 20 +++++++++++++++++++ frontends/ol-utilities/src/date/format.ts | 4 +++- frontends/ol-utilities/src/index.ts | 2 ++ frontends/ol-utilities/src/ssr/NoSSR.tsx | 16 +++++++++++++++ 11 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 frontends/ol-utilities/src/date/LocalDate.tsx create mode 100644 frontends/ol-utilities/src/ssr/NoSSR.tsx diff --git a/frontends/main/package.json b/frontends/main/package.json index 9cf7375c7a..6511220ed6 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "PORT=${PORT:-8062} next dev", + "dev": "PORT=${PORT:-8062} TZ=UTC next dev", "build": "next build", "build:no-lint": "next build --no-lint", - "start": "next start", + "start": "TZ=UTC next start", "lint": "next lint" }, "dependencies": { diff --git a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx index bcf7d7227f..2b9b37abbb 100644 --- a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx @@ -13,7 +13,7 @@ import { NewsEventsListFeedTypeEnum, } from "api/hooks/newsEvents" import type { NewsFeedItem, EventFeedItem } from "api/v0" -import { formatDate } from "ol-utilities" +import { LocalDate } from "ol-utilities" import { RiArrowRightSLine } from "@remixicon/react" import Link from "next/link" @@ -196,7 +196,7 @@ const Story: React.FC<{ item: NewsFeedItem; mobile: boolean }> = ({ {item.title} - Published: {formatDate(item.news_details?.publish_date)} + Published: ) @@ -226,16 +226,16 @@ const NewsEventsSection: React.FC = () => { - {formatDate( - (item as EventFeedItem).event_details?.event_datetime, - "D", - )} + - {formatDate( - (item as EventFeedItem).event_details?.event_datetime, - "MMM", - )} + diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx index 8db2fffc68..c0aec628f1 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { LearningResource } from "api" import { - formatDate, + LocalDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, getLearningResourcePrices, @@ -149,7 +149,8 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({ const format = size === "small" ? "MMM DD, YYYY" : "MMMM DD, YYYY" const formatted = anytime ? "Anytime" - : startDate && formatDate(startDate, format) + : startDate && + if (!formatted) return null const showLabel = size !== "small" || anytime diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx index f68b78baa4..65b08b0cdb 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { ResourceTypeEnum, LearningResource } from "api" import { - formatDate, + LocalDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, pluralize, @@ -151,7 +151,7 @@ export const StartDate: React.FC<{ resource: LearningResource }> = ({ const startDate = getResourceDate(resource) const formatted = anytime ? "Anytime" - : startDate && formatDate(startDate, "MMMM DD, YYYY") + : startDate && if (!formatted) return null return ( diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx index 57bdaf0843..ea1663f41c 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx @@ -8,6 +8,7 @@ import { getDisplayPrice, getRunPrices, showStartAnytime, + NoSSR, } from "ol-utilities" const DifferingRuns = styled.div({ @@ -103,7 +104,9 @@ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({ {resource.runs?.map((run, index) => ( - {formatRunDate(run, asTaughtIn)} + + {formatRunDate(run, asTaughtIn)} + {run.resource_prices && ( {getDisplayPrice(getRunPrices(run)["course"])} diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 6908649c75..2141d8c52e 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -24,6 +24,7 @@ import { formatRunDate, getLearningResourcePrices, showStartAnytime, + NoSSR, } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" import DifferingRunsTable from "./DifferingRunsTable" @@ -255,7 +256,11 @@ const INFO_ITEMS: InfoItemConfig = [ const totalDatesWithRuns = resource.runs?.filter((run) => run.start_date !== null).length || 0 if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) { - return + return ( + + + + ) } else return null }, }, diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx index 09005f595a..ed27f0ef29 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx @@ -7,6 +7,7 @@ import { ButtonLink } from "../Button/Button" import type { LearningResource, LearningResourceRun } from "api" import { ResourceTypeEnum, PlatformEnum } from "api" import { + NoSSR, formatDate, capitalize, DEFAULT_RESOURCE_IMG, @@ -299,7 +300,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { return ( = ({ .map((run) => { return { value: run.id.toString(), - label: formatRunDate(run, asTaughtIn), + label: {formatRunDate(run, asTaughtIn)}, } }) ?? [] @@ -415,7 +416,7 @@ const LearningResourceExpandedV1: React.FC = ({ return ( {label} - {formatted ?? ""} + {formatted ?? ""} ) } diff --git a/frontends/ol-utilities/src/date/LocalDate.tsx b/frontends/ol-utilities/src/date/LocalDate.tsx new file mode 100644 index 0000000000..1ebdb9c636 --- /dev/null +++ b/frontends/ol-utilities/src/date/LocalDate.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { NoSSR } from "../ssr/NoSSR" +import { formatDate } from "./format" + +type LocalDateProps = { + date?: string | Date | null + /** + * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ + */ + format?: string +} + +/* Component to render dates only on the client as these are displayed + * according to the user's locale (generally, not all Moment.js format tokens + * are localized) causing an error due to hydration mismatch. + */ +export const LocalDate = ({ date, format = "MMM D, YYYY" }: LocalDateProps) => { + if (!date) return null + return {formatDate(date, format)} +} diff --git a/frontends/ol-utilities/src/date/format.ts b/frontends/ol-utilities/src/date/format.ts index 3bd6206487..706d5cbef6 100644 --- a/frontends/ol-utilities/src/date/format.ts +++ b/frontends/ol-utilities/src/date/format.ts @@ -1,12 +1,14 @@ import moment from "moment" +/* Instances must be wrapped in to avoid SSR hydration mismatches. + */ export const formatDate = ( /** * Date string or date. */ date: string | Date, /** - * A momentjs format string. See https://momentjs.com/docs/#/displaying/format/ + * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ */ format = "MMM D, YYYY", ) => { diff --git a/frontends/ol-utilities/src/index.ts b/frontends/ol-utilities/src/index.ts index eefe535703..39f05c6eb6 100644 --- a/frontends/ol-utilities/src/index.ts +++ b/frontends/ol-utilities/src/index.ts @@ -6,6 +6,7 @@ export * from "./styles" export * from "./date/format" +export * from "./date/LocalDate" export * from "./learning-resources/learning-resources" export * from "./learning-resources/pricing" export * from "./strings/html" @@ -14,3 +15,4 @@ export * from "./hooks" export * from "./querystrings" export * from "./lib" export * from "./images/backgroundImages" +export * from "./ssr/NoSSR" diff --git a/frontends/ol-utilities/src/ssr/NoSSR.tsx b/frontends/ol-utilities/src/ssr/NoSSR.tsx new file mode 100644 index 0000000000..4a3d344e2b --- /dev/null +++ b/frontends/ol-utilities/src/ssr/NoSSR.tsx @@ -0,0 +1,16 @@ +import React, { useState, useEffect, ReactNode } from "react" + +type NoSSRProps = { + children: ReactNode + onSSR?: ReactNode +} + +export const NoSSR: React.FC = ({ children, onSSR = <> }) => { + const [isClient, setClient] = useState(false) + + useEffect(() => { + setClient(true) + }, []) + + return <>{isClient ? children : onSSR} +} From ffa1098fd98c185cee2070b0f7494bafe69c07e9 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:32:57 +0100 Subject: [PATCH 14/16] Revert "LocalDate and NoSSR components to render localized dates only on client" This reverts commit b4ccd6d361a8e54d25eb51ef25e5f4e3c0cad774. --- frontends/main/package.json | 4 ++-- .../app-pages/HomePage/NewsEventsSection.tsx | 20 +++++++++---------- .../LearningResourceCard.tsx | 5 ++--- .../LearningResourceListCard.tsx | 4 ++-- .../DifferingRunsTable.tsx | 5 +---- .../InfoSectionV2.tsx | 7 +------ .../LearningResourceExpandedV1.tsx | 7 +++---- frontends/ol-utilities/src/date/LocalDate.tsx | 20 ------------------- frontends/ol-utilities/src/date/format.ts | 4 +--- frontends/ol-utilities/src/index.ts | 2 -- frontends/ol-utilities/src/ssr/NoSSR.tsx | 16 --------------- 11 files changed, 22 insertions(+), 72 deletions(-) delete mode 100644 frontends/ol-utilities/src/date/LocalDate.tsx delete mode 100644 frontends/ol-utilities/src/ssr/NoSSR.tsx diff --git a/frontends/main/package.json b/frontends/main/package.json index 6511220ed6..9cf7375c7a 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "PORT=${PORT:-8062} TZ=UTC next dev", + "dev": "PORT=${PORT:-8062} next dev", "build": "next build", "build:no-lint": "next build --no-lint", - "start": "TZ=UTC next start", + "start": "next start", "lint": "next lint" }, "dependencies": { diff --git a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx index 2b9b37abbb..bcf7d7227f 100644 --- a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx @@ -13,7 +13,7 @@ import { NewsEventsListFeedTypeEnum, } from "api/hooks/newsEvents" import type { NewsFeedItem, EventFeedItem } from "api/v0" -import { LocalDate } from "ol-utilities" +import { formatDate } from "ol-utilities" import { RiArrowRightSLine } from "@remixicon/react" import Link from "next/link" @@ -196,7 +196,7 @@ const Story: React.FC<{ item: NewsFeedItem; mobile: boolean }> = ({ {item.title} - Published: + Published: {formatDate(item.news_details?.publish_date)} ) @@ -226,16 +226,16 @@ const NewsEventsSection: React.FC = () => { - + {formatDate( + (item as EventFeedItem).event_details?.event_datetime, + "D", + )} - + {formatDate( + (item as EventFeedItem).event_details?.event_datetime, + "MMM", + )} diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx index c0aec628f1..8db2fffc68 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { LearningResource } from "api" import { - LocalDate, + formatDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, getLearningResourcePrices, @@ -149,8 +149,7 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({ const format = size === "small" ? "MMM DD, YYYY" : "MMMM DD, YYYY" const formatted = anytime ? "Anytime" - : startDate && - + : startDate && formatDate(startDate, format) if (!formatted) return null const showLabel = size !== "small" || anytime diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx index 65b08b0cdb..f68b78baa4 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { ResourceTypeEnum, LearningResource } from "api" import { - LocalDate, + formatDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, pluralize, @@ -151,7 +151,7 @@ export const StartDate: React.FC<{ resource: LearningResource }> = ({ const startDate = getResourceDate(resource) const formatted = anytime ? "Anytime" - : startDate && + : startDate && formatDate(startDate, "MMMM DD, YYYY") if (!formatted) return null return ( diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx index ea1663f41c..57bdaf0843 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx @@ -8,7 +8,6 @@ import { getDisplayPrice, getRunPrices, showStartAnytime, - NoSSR, } from "ol-utilities" const DifferingRuns = styled.div({ @@ -104,9 +103,7 @@ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({ {resource.runs?.map((run, index) => ( - - {formatRunDate(run, asTaughtIn)} - + {formatRunDate(run, asTaughtIn)} {run.resource_prices && ( {getDisplayPrice(getRunPrices(run)["course"])} diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 2141d8c52e..6908649c75 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -24,7 +24,6 @@ import { formatRunDate, getLearningResourcePrices, showStartAnytime, - NoSSR, } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" import DifferingRunsTable from "./DifferingRunsTable" @@ -256,11 +255,7 @@ const INFO_ITEMS: InfoItemConfig = [ const totalDatesWithRuns = resource.runs?.filter((run) => run.start_date !== null).length || 0 if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) { - return ( - - - - ) + return } else return null }, }, diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx index ed27f0ef29..09005f595a 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx @@ -7,7 +7,6 @@ import { ButtonLink } from "../Button/Button" import type { LearningResource, LearningResourceRun } from "api" import { ResourceTypeEnum, PlatformEnum } from "api" import { - NoSSR, formatDate, capitalize, DEFAULT_RESOURCE_IMG, @@ -300,7 +299,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { return ( = ({ .map((run) => { return { value: run.id.toString(), - label: {formatRunDate(run, asTaughtIn)}, + label: formatRunDate(run, asTaughtIn), } }) ?? [] @@ -416,7 +415,7 @@ const LearningResourceExpandedV1: React.FC = ({ return ( {label} - {formatted ?? ""} + {formatted ?? ""} ) } diff --git a/frontends/ol-utilities/src/date/LocalDate.tsx b/frontends/ol-utilities/src/date/LocalDate.tsx deleted file mode 100644 index 1ebdb9c636..0000000000 --- a/frontends/ol-utilities/src/date/LocalDate.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react" -import { NoSSR } from "../ssr/NoSSR" -import { formatDate } from "./format" - -type LocalDateProps = { - date?: string | Date | null - /** - * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ - */ - format?: string -} - -/* Component to render dates only on the client as these are displayed - * according to the user's locale (generally, not all Moment.js format tokens - * are localized) causing an error due to hydration mismatch. - */ -export const LocalDate = ({ date, format = "MMM D, YYYY" }: LocalDateProps) => { - if (!date) return null - return {formatDate(date, format)} -} diff --git a/frontends/ol-utilities/src/date/format.ts b/frontends/ol-utilities/src/date/format.ts index 706d5cbef6..3bd6206487 100644 --- a/frontends/ol-utilities/src/date/format.ts +++ b/frontends/ol-utilities/src/date/format.ts @@ -1,14 +1,12 @@ import moment from "moment" -/* Instances must be wrapped in to avoid SSR hydration mismatches. - */ export const formatDate = ( /** * Date string or date. */ date: string | Date, /** - * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ + * A momentjs format string. See https://momentjs.com/docs/#/displaying/format/ */ format = "MMM D, YYYY", ) => { diff --git a/frontends/ol-utilities/src/index.ts b/frontends/ol-utilities/src/index.ts index 39f05c6eb6..eefe535703 100644 --- a/frontends/ol-utilities/src/index.ts +++ b/frontends/ol-utilities/src/index.ts @@ -6,7 +6,6 @@ export * from "./styles" export * from "./date/format" -export * from "./date/LocalDate" export * from "./learning-resources/learning-resources" export * from "./learning-resources/pricing" export * from "./strings/html" @@ -15,4 +14,3 @@ export * from "./hooks" export * from "./querystrings" export * from "./lib" export * from "./images/backgroundImages" -export * from "./ssr/NoSSR" diff --git a/frontends/ol-utilities/src/ssr/NoSSR.tsx b/frontends/ol-utilities/src/ssr/NoSSR.tsx deleted file mode 100644 index 4a3d344e2b..0000000000 --- a/frontends/ol-utilities/src/ssr/NoSSR.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { useState, useEffect, ReactNode } from "react" - -type NoSSRProps = { - children: ReactNode - onSSR?: ReactNode -} - -export const NoSSR: React.FC = ({ children, onSSR = <> }) => { - const [isClient, setClient] = useState(false) - - useEffect(() => { - setClient(true) - }, []) - - return <>{isClient ? children : onSSR} -} From 85fe93728f737b93c931ada46bbf36cdbf976a9f Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:10:00 +0100 Subject: [PATCH 15/16] LocalDate and NoSSR components to render localized dates only on client (#1831) * LocalDate and NoSSR components to render localized dates only on client * Remove unnecessary React.Fragment --- frontends/main/package.json | 4 ++-- .../app-pages/HomePage/NewsEventsSection.tsx | 20 +++++++++---------- .../LearningResourceCard.tsx | 5 +++-- .../LearningResourceListCard.tsx | 4 ++-- .../DifferingRunsTable.tsx | 5 ++++- .../InfoSectionV2.tsx | 7 ++++++- .../LearningResourceExpandedV1.tsx | 7 ++++--- frontends/ol-utilities/src/date/LocalDate.tsx | 20 +++++++++++++++++++ frontends/ol-utilities/src/date/format.ts | 4 +++- frontends/ol-utilities/src/index.ts | 2 ++ frontends/ol-utilities/src/ssr/NoSSR.tsx | 16 +++++++++++++++ 11 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 frontends/ol-utilities/src/date/LocalDate.tsx create mode 100644 frontends/ol-utilities/src/ssr/NoSSR.tsx diff --git a/frontends/main/package.json b/frontends/main/package.json index 9cf7375c7a..6511220ed6 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "PORT=${PORT:-8062} next dev", + "dev": "PORT=${PORT:-8062} TZ=UTC next dev", "build": "next build", "build:no-lint": "next build --no-lint", - "start": "next start", + "start": "TZ=UTC next start", "lint": "next lint" }, "dependencies": { diff --git a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx index bcf7d7227f..2b9b37abbb 100644 --- a/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx @@ -13,7 +13,7 @@ import { NewsEventsListFeedTypeEnum, } from "api/hooks/newsEvents" import type { NewsFeedItem, EventFeedItem } from "api/v0" -import { formatDate } from "ol-utilities" +import { LocalDate } from "ol-utilities" import { RiArrowRightSLine } from "@remixicon/react" import Link from "next/link" @@ -196,7 +196,7 @@ const Story: React.FC<{ item: NewsFeedItem; mobile: boolean }> = ({ {item.title} - Published: {formatDate(item.news_details?.publish_date)} + Published: ) @@ -226,16 +226,16 @@ const NewsEventsSection: React.FC = () => { - {formatDate( - (item as EventFeedItem).event_details?.event_datetime, - "D", - )} + - {formatDate( - (item as EventFeedItem).event_details?.event_datetime, - "MMM", - )} + diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx index 8db2fffc68..c0aec628f1 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { LearningResource } from "api" import { - formatDate, + LocalDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, getLearningResourcePrices, @@ -149,7 +149,8 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({ const format = size === "small" ? "MMM DD, YYYY" : "MMMM DD, YYYY" const formatted = anytime ? "Anytime" - : startDate && formatDate(startDate, format) + : startDate && + if (!formatted) return null const showLabel = size !== "small" || anytime diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx index f68b78baa4..65b08b0cdb 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx @@ -9,7 +9,7 @@ import { } from "@remixicon/react" import { ResourceTypeEnum, LearningResource } from "api" import { - formatDate, + LocalDate, getReadableResourceType, DEFAULT_RESOURCE_IMG, pluralize, @@ -151,7 +151,7 @@ export const StartDate: React.FC<{ resource: LearningResource }> = ({ const startDate = getResourceDate(resource) const formatted = anytime ? "Anytime" - : startDate && formatDate(startDate, "MMMM DD, YYYY") + : startDate && if (!formatted) return null return ( diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx index 57bdaf0843..ea1663f41c 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx @@ -8,6 +8,7 @@ import { getDisplayPrice, getRunPrices, showStartAnytime, + NoSSR, } from "ol-utilities" const DifferingRuns = styled.div({ @@ -103,7 +104,9 @@ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({ {resource.runs?.map((run, index) => ( - {formatRunDate(run, asTaughtIn)} + + {formatRunDate(run, asTaughtIn)} + {run.resource_prices && ( {getDisplayPrice(getRunPrices(run)["course"])} diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx index 6908649c75..2141d8c52e 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx @@ -24,6 +24,7 @@ import { formatRunDate, getLearningResourcePrices, showStartAnytime, + NoSSR, } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" import DifferingRunsTable from "./DifferingRunsTable" @@ -255,7 +256,11 @@ const INFO_ITEMS: InfoItemConfig = [ const totalDatesWithRuns = resource.runs?.filter((run) => run.start_date !== null).length || 0 if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) { - return + return ( + + + + ) } else return null }, }, diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx index 09005f595a..ed27f0ef29 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx @@ -7,6 +7,7 @@ import { ButtonLink } from "../Button/Button" import type { LearningResource, LearningResourceRun } from "api" import { ResourceTypeEnum, PlatformEnum } from "api" import { + NoSSR, formatDate, capitalize, DEFAULT_RESOURCE_IMG, @@ -299,7 +300,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { return ( = ({ .map((run) => { return { value: run.id.toString(), - label: formatRunDate(run, asTaughtIn), + label: {formatRunDate(run, asTaughtIn)}, } }) ?? [] @@ -415,7 +416,7 @@ const LearningResourceExpandedV1: React.FC = ({ return ( {label} - {formatted ?? ""} + {formatted ?? ""} ) } diff --git a/frontends/ol-utilities/src/date/LocalDate.tsx b/frontends/ol-utilities/src/date/LocalDate.tsx new file mode 100644 index 0000000000..1ebdb9c636 --- /dev/null +++ b/frontends/ol-utilities/src/date/LocalDate.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { NoSSR } from "../ssr/NoSSR" +import { formatDate } from "./format" + +type LocalDateProps = { + date?: string | Date | null + /** + * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ + */ + format?: string +} + +/* Component to render dates only on the client as these are displayed + * according to the user's locale (generally, not all Moment.js format tokens + * are localized) causing an error due to hydration mismatch. + */ +export const LocalDate = ({ date, format = "MMM D, YYYY" }: LocalDateProps) => { + if (!date) return null + return {formatDate(date, format)} +} diff --git a/frontends/ol-utilities/src/date/format.ts b/frontends/ol-utilities/src/date/format.ts index 3bd6206487..706d5cbef6 100644 --- a/frontends/ol-utilities/src/date/format.ts +++ b/frontends/ol-utilities/src/date/format.ts @@ -1,12 +1,14 @@ import moment from "moment" +/* Instances must be wrapped in to avoid SSR hydration mismatches. + */ export const formatDate = ( /** * Date string or date. */ date: string | Date, /** - * A momentjs format string. See https://momentjs.com/docs/#/displaying/format/ + * A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/ */ format = "MMM D, YYYY", ) => { diff --git a/frontends/ol-utilities/src/index.ts b/frontends/ol-utilities/src/index.ts index eefe535703..39f05c6eb6 100644 --- a/frontends/ol-utilities/src/index.ts +++ b/frontends/ol-utilities/src/index.ts @@ -6,6 +6,7 @@ export * from "./styles" export * from "./date/format" +export * from "./date/LocalDate" export * from "./learning-resources/learning-resources" export * from "./learning-resources/pricing" export * from "./strings/html" @@ -14,3 +15,4 @@ export * from "./hooks" export * from "./querystrings" export * from "./lib" export * from "./images/backgroundImages" +export * from "./ssr/NoSSR" diff --git a/frontends/ol-utilities/src/ssr/NoSSR.tsx b/frontends/ol-utilities/src/ssr/NoSSR.tsx new file mode 100644 index 0000000000..ca93c8596f --- /dev/null +++ b/frontends/ol-utilities/src/ssr/NoSSR.tsx @@ -0,0 +1,16 @@ +import React, { useState, useEffect, ReactNode } from "react" + +type NoSSRProps = { + children: ReactNode + onSSR?: ReactNode +} + +export const NoSSR: React.FC = ({ children, onSSR = null }) => { + const [isClient, setClient] = useState(false) + + useEffect(() => { + setClient(true) + }, []) + + return isClient ? children : onSSR +} From 143b2fdae99a74e8981f3e76966e300c31a2ee9e Mon Sep 17 00:00:00 2001 From: Doof Date: Mon, 18 Nov 2024 18:13:03 +0000 Subject: [PATCH 16/16] Release 0.25.0 --- RELEASE.rst | 19 +++++++++++++++++++ main/settings.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 961fd6384f..50c9fd0f84 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,25 @@ Release Notes ============= +Version 0.25.0 +-------------- + +- LocalDate and NoSSR components to render localized dates only on client (#1831) +- Revert "LocalDate and NoSSR components to render localized dates only on client" +- LocalDate and NoSSR components to render localized dates only on client +- v2 learning resource drawer formats and location (#1826) +- v2 drawer certification updates (#1823) +- Mechanism to sync server prefetch with client API calls (#1798) +- show more button for v2 drawer dates (#1809) +- Add data-ph- elements to CTA buttons (#1821) +- Endpoints for userlist/learningpath memberships (#1808) +- Clear resource_type filter when leaving Learning Materials search tab (#1780) +- Update opensearchproject/opensearch Docker tag to v2.18.0 (#1812) +- Update dependency postcss-styled-syntax to ^0.7.0 (#1811) +- Update dependency ruff to v0.7.3 (#1810) +- Update dependency @chromatic-com/storybook to v3 (#1764) +- learning resource drawer v2 run comparison table (#1782) + Version 0.24.3 (Released November 14, 2024) -------------- diff --git a/main/settings.py b/main/settings.py index 8b95c15ec9..70e30a6151 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.24.3" +VERSION = "0.25.0" log = logging.getLogger()