Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Release Notes
=============

Version 0.20.4
--------------

- add is_incomplete_or_stale to default sort (#1641)
- set default minimum score cutoff (#1642)
- Adds base infra for the Unified Ecommerce frontend (#1634)
- reset search page in SearchField (#1647)
- updating email template with new logo (#1638)

Version 0.20.3 (Released October 03, 2024)
--------------

Expand Down
61 changes: 61 additions & 0 deletions data_fixtures/migrations/0015_unit_page_copy_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 4.2.14 on 2024-07-16 17:30

from django.db import migrations

fixtures = [
{
"name": "mitpe",
"offeror_configuration": {
"value_prop": (
"MIT Professional Education is a leader in technology and "
"engineering education for working professionals pursuing "
"career advancement, and organizations seeking to meet modern-day "
"challenges by expanding the knowledge and skills of their employees. "
"Courses are delivered in a range of formats—in-person (on-campus "
"and live online), online, and through hybrid approaches—to "
"meet the needs of today's learners."
),
},
"channel_configuration": {
"sub_heading": (
"MIT Professional Education is a leader in technology and "
"engineering education for working professionals pursuing "
"career advancement, and organizations seeking to meet modern-day "
"challenges by expanding the knowledge and skills of their employees. "
"Courses are delivered in a range of formats—in-person (on-campus "
"and live online), online, and through hybrid approaches—to "
"meet the needs of today's learners."
),
},
},
]


def update_copy(apps, schema_editor):
Channel = apps.get_model("channels", "Channel")
LearningResourceOfferor = apps.get_model(
"learning_resources", "LearningResourceOfferor"
)
for fixture in fixtures:
channel_configuration_updates = fixture["channel_configuration"]
offeror_configuration_updates = fixture["offeror_configuration"]
channel = Channel.objects.get(name=fixture["name"])
if Channel.objects.filter(name=fixture["name"]).exists():
for key, val in channel_configuration_updates.items():
channel.configuration[key] = val
channel.save()
if LearningResourceOfferor.objects.filter(code=fixture["name"]).exists():
offeror = LearningResourceOfferor.objects.get(code=fixture["name"])
for key, val in offeror_configuration_updates.items():
setattr(offeror, key, val)
offeror.save()


class Migration(migrations.Migration):
dependencies = [
("data_fixtures", "0014_add_department_SP"),
]

operations = [
migrations.RunPython(update_copy, migrations.RunPython.noop),
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed frontends/mit-learn/public/images/mit-logo-learn.jpg
Binary file not shown.
6 changes: 6 additions & 0 deletions frontends/mit-learn/src/common/feature_flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Feature flags for the app. These should correspond to the flag that's set up
// in PostHog.

export enum FeatureFlags {
EnableEcommerce = "enable-ecommerce",
}
2 changes: 2 additions & 0 deletions frontends/mit-learn/src/common/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ export const SEARCH_PROGRAM = querifiedSearchUrl({
export const SEARCH_LEARNING_MATERIAL = querifiedSearchUrl({
resource_category: "learning_material",
})

export const ECOMMERCE_CART = "/cart/" as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { ForbiddenError } from "@/common/permissions"
import { FeatureFlags } from "@/common/feature_flags"

type EcommerceFeatureProps = {
children: React.ReactNode
}

/**
* Simple wrapper to standardize the feature flag check for ecommerce UI pages.
* If the flag is enabled, display the children; if not, throw a ForbiddenError
* like you'd get for an unauthenticated route.
*
* There's a PostHogFeature component that is provided but went this route
* because it seemed to be inconsistent - sometimes having the flag enabled
* resulted in it tossing to the error page.
*
* Set the feature flag here using the enum, and then make sure it's also
* defined in commmon/feature_flags too.
*/

const EcommerceFeature: React.FC<EcommerceFeatureProps> = ({ children }) => {
const ecommFlag = useFeatureFlagEnabled(FeatureFlags.EnableEcommerce)

if (ecommFlag === false) {
throw new ForbiddenError("Not enabled.")
}

return ecommFlag ? children : null
}

export default EcommerceFeature
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react"
import { SearchInput } from "ol-components"
import type { SearchInputProps, SearchSubmissionEvent } from "ol-components"
import { usePostHog } from "posthog-js/react"

type SearchFieldProps = SearchInputProps & {
onSubmit: (event: SearchSubmissionEvent) => void
setPage: (page: number) => void
}

const { POSTHOG } = APP_SETTINGS

/**
* A wrapper around SearchInput that handles a little application logic like
* - resetting search page to 1 on submission
* - firing tracking events
*/
const SearchField: React.FC<SearchFieldProps> = ({
onSubmit,
setPage,
...others
}) => {
const posthog = usePostHog()
const handleSubmit: SearchInputProps["onSubmit"] = (
event,
{ isEnter } = {},
) => {
onSubmit(event)
setPage(1)
if (POSTHOG?.api_key) {
posthog.capture("search_update", { isEnter: isEnter })
}
}

return <SearchInput onSubmit={handleSubmit} {...others} />
}

export { SearchField }
43 changes: 42 additions & 1 deletion frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen, within, waitFor, renderTestApp } from "@/test-utils"
import { screen, within, waitFor, renderTestApp, user } from "@/test-utils"
import { setMockResponse, urls, factories, makeRequest } from "api/test-utils"
import type { LearningResourcesSearchResponse } from "api"
import invariant from "tiny-invariant"
Expand Down Expand Up @@ -264,4 +264,45 @@ describe("ChannelSearch", () => {
}
},
)

test("Submitting search text updates URL correctly", async () => {
const resources = factories.learningResources.resources({
count: 10,
}).results
const { channel } = setMockApiResponses({
search: {
count: 1000,
metadata: {
aggregations: {
resource_type: [
{ key: "course", doc_count: 100 },
{ key: "podcast", doc_count: 200 },
{ key: "program", doc_count: 300 },
{ key: "irrelevant", doc_count: 400 },
],
},
suggestions: [],
},
results: resources,
},
})
setMockResponse.get(urls.userMe.get(), {})

const initialSearch = "?q=meow&page=2"
const finalSearch = "?q=woof"

const { location } = renderTestApp({
url: `/c/${channel.channel_type}/${channel.name}${initialSearch}`,
})

const queryInput = await screen.findByRole<HTMLInputElement>("textbox", {
name: "Search for",
})
expect(queryInput.value).toBe("meow")
await user.clear(queryInput)
await user.paste("woof")
expect(location.current.search).toBe(initialSearch)
await user.click(screen.getByRole("button", { name: "Search" }))
expect(location.current.search).toBe(finalSearch)
})
})
8 changes: 5 additions & 3 deletions frontends/mit-learn/src/pages/ChannelPage/ChannelSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import type {
} from "@mitodl/course-search-utils"
import { useSearchParams } from "@mitodl/course-search-utils/react-router"
import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay"
import { Container, SearchInput, styled, VisuallyHidden } from "ol-components"
import { Container, styled, VisuallyHidden } from "ol-components"
import { SearchField } from "@/page-components/SearchField/SearchField"

import { getFacetManifest } from "@/pages/SearchPage/SearchPage"

Expand All @@ -30,7 +31,7 @@ const SearchInputContainer = styled(Container)(({ theme }) => ({
},
}))

const StyledSearchInput = styled(SearchInput)({
const StyledSearchField = styled(SearchField)({
width: "624px",
})

Expand Down Expand Up @@ -172,7 +173,7 @@ const ChannelSearch: React.FC<ChannelSearchProps> = ({
<section>
<VisuallyHidden as="h2">Search within {channelTitle}</VisuallyHidden>
<SearchInputContainer>
<StyledSearchInput
<StyledSearchField
value={currentText}
size="large"
onChange={(e) => setCurrentText(e.target.value)}
Expand All @@ -182,6 +183,7 @@ const ChannelSearch: React.FC<ChannelSearchProps> = ({
onClear={() => {
setCurrentTextAndQuery("")
}}
setPage={setPage}
/>
</SearchInputContainer>

Expand Down
65 changes: 65 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/CartPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { renderTestApp, waitFor, setMockResponse } from "../../test-utils"
import { urls } from "api/test-utils"
import * as commonUrls from "@/common/urls"
import { Permissions } from "@/common/permissions"
import { login } from "@/common/urls"
import { useFeatureFlagEnabled } from "posthog-js/react"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)

const oldWindowLocation = window.location

beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).location

window.location = Object.defineProperties({} as Location, {
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn(),
},
})
})

afterAll(() => {
window.location = oldWindowLocation
})

describe("CartPage", () => {
;["on", "off"].forEach((testCase: string) => {
test(`Renders when logged in and feature flag is ${testCase}`, async () => {
setMockResponse.get(urls.userMe.get(), {
[Permissions.Authenticated]: true,
})
mockedUseFeatureFlagEnabled.mockReturnValue(testCase === "on")

renderTestApp({
url: commonUrls.ECOMMERCE_CART,
})
await waitFor(() => {
testCase === "on"
? expect(document.title).toBe("Shopping Cart | MIT Learn")
: expect(document.title).not.toBe("Shopping Cart | MIT Learn")
})
})
})

test("Sends to login page when logged out", async () => {
setMockResponse.get(urls.userMe.get(), {
[Permissions.Authenticated]: false,
})
const expectedUrl = login({
pathname: "/cart/",
})

renderTestApp({
url: commonUrls.ECOMMERCE_CART,
})

await waitFor(() => {
expect(window.location.assign).toHaveBeenCalledWith(expectedUrl)
})
})
})
31 changes: 31 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/CartPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react"
import { Breadcrumbs, Container, Typography } from "ol-components"
import EcommerceFeature from "@/page-components/EcommerceFeature/EcommerceFeature"
import MetaTags from "@/page-components/MetaTags/MetaTags"
import * as urls from "@/common/urls"

const CartPage: React.FC = () => {
return (
<EcommerceFeature>
<Container>
<MetaTags title="Shopping Cart" />
<Breadcrumbs
variant="light"
ancestors={[{ href: urls.HOME, label: "Home" }]}
current="Shopping Cart"
/>

<Typography component="h1" variant="h3">
Shopping Cart
</Typography>

<Typography>
The shopping cart layout should go here, if you're allowed to see
this.
</Typography>
</Container>
</EcommerceFeature>
)
}

export default CartPage
9 changes: 9 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Unified Ecommerce in MIT Learn

The front end for the Unified Ecommerce system lives in MIT Learn. So, pages that exist here are designed to talk to Unified Ecommerce rather than to the Learn system.

There's a few functional pieces here:

- **Cart** - Displays the user's cart, and provides some additional functionality for that (item management, discount application, etc.)
- **Receipts** - Allows the user to display their order history and view receipts from their purchases, including historical ones from other systems.
- **Financial Assistance** - For learning resources that support it, the learner side of the financial assistance request system lives here. (Approvals do not.)
Loading
Loading