Skip to content
Closed
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
6 changes: 6 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release Notes
=============

Version 0.19.4 (Released September 25, 2024)
--------------

- new -> recently added (#1594)
- Pace and format fields for learning resources (#1588)

Version 0.19.3 (Released September 23, 2024)
--------------

Expand Down
186 changes: 186 additions & 0 deletions frontends/api/src/generated/v1/api.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import type {
BooleanFacetKey,
} from "@mitodl/course-search-utils"
import { BOOLEAN_FACET_NAMES } from "@mitodl/course-search-utils"
import { Skeleton, styled } from "ol-components"
import { Skeleton, styled, SimpleSelect } from "ol-components"
import type { SimpleSelectOption } from "ol-components"
import { StyledSelect } from "@/page-components/SearchDisplay/SearchDisplay"

const StyledSkeleton = styled(Skeleton)`
display: inline-flex;
Expand Down Expand Up @@ -117,7 +116,7 @@ const AvailableFacetsDropdowns: React.FC<

return (
facetItems.length && (
<StyledSelect
<SimpleSelect
key={facetSetting.name}
value={displayValue}
multiple={isMultiple}
Expand Down
2 changes: 1 addition & 1 deletion frontends/main/src/page-components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ const navData: NavData = {
title: "DISCOVER LEARNING RESOURCES",
items: [
{
title: "New",
title: "Recently Added",
icon: <RiFileAddLine />,
href: SEARCH_NEW,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ import type { TabConfig } from "./ResourceCategoryTabs"
import { ResourceCard } from "../ResourceCard/ResourceCard"
import { useUserMe } from "api/hooks/user"

export const StyledSelect = styled(SimpleSelect)`
min-width: 160px;
`

const StyledResourceTabs = styled(ResourceCategoryTabs.TabList)`
margin-top: 0 px;
`
Expand Down Expand Up @@ -469,7 +465,7 @@ const SORT_OPTIONS = [
value: "",
},
{
label: "New",
label: "Recently Added",
value: "new",
},
{
Expand Down Expand Up @@ -601,7 +597,7 @@ const SearchDisplay: React.FC<SearchDisplayProps> = ({
}

const searchModeDropdown = (
<StyledSelect
<SimpleSelect
size="small"
value={searchParams.get("search_mode") || DEFAULT_SEARCH_MODE}
onChange={(e) =>
Expand All @@ -620,7 +616,7 @@ const SearchDisplay: React.FC<SearchDisplayProps> = ({
)

const sortDropdown = (
<StyledSelect
<SimpleSelect
size="small"
value={requestParams.sortby || ""}
onChange={(e) => setParamValue("sortby", e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useMemo } from "react"
import { Popover, Typography, styled, Button } from "ol-components"
import type { PopoverProps } from "ol-components"
import { getSearchParamMap } from "@/common/utils"

import { SignupPopover } from "../SignupPopover/SignupPopover"

import { useUserMe } from "api/hooks/user"
import { SourceTypeEnum } from "api"
import {
useSearchSubscriptionCreate,
useSearchSubscriptionDelete,
useSearchSubscriptionList,
} from "api/hooks/searchSubscription"

const StyledPopover = styled(Popover)({
width: "300px",
maxWidth: "100vw",
})
const HeaderText = styled(Typography)(({ theme }) => ({
color: theme.custom.colors.darkGray2,
marginBottom: "8px",
...theme.typography.subtitle2,
}))
const BodyText = styled(Typography)(({ theme }) => ({
...theme.typography.body2,
color: theme.custom.colors.silverGrayDark,
marginBottom: "16px",
}))

const Footer = styled.div({
display: "flex",
justifyContent: "end",
gap: "16px",
})

interface FollowPopoverProps
extends Pick<PopoverProps, "anchorEl" | "onClose" | "placement"> {
itemName?: string
searchParams: URLSearchParams
sourceType: SourceTypeEnum
}

const FollowPopover: React.FC<FollowPopoverProps> = ({
itemName,
searchParams,
sourceType,
...props
}) => {
const { data: user } = useUserMe()
const subscribeParams: Record<string, string[] | string> = useMemo(() => {
return { source_type: sourceType, ...getSearchParamMap(searchParams) }
}, [searchParams, sourceType])

const subscriptionDelete = useSearchSubscriptionDelete()
const subscriptionCreate = useSearchSubscriptionCreate()
const subscriptionList = useSearchSubscriptionList(subscribeParams, {
enabled: !!user?.is_authenticated,
})
const unsubscribe = subscriptionDelete.mutate
const subscriptionId = subscriptionList.data?.[0]?.id

const isSubscribed = !!subscriptionId
const handleFollowAction = async (): Promise<void> => {
props.onClose()
if (!isSubscribed) {
await subscriptionCreate.mutateAsync({
PercolateQuerySubscriptionRequestRequest: subscribeParams,
})
} else {
unsubscribe(subscriptionId)
}
}

if (user?.is_authenticated && subscriptionList.isLoading) return null
if (!user) return null
if (!user?.is_authenticated) {
return <SignupPopover {...props}></SignupPopover>
}

if (isSubscribed) {
return (
<StyledPopover {...props} open={!!props.anchorEl}>
<HeaderText variant="subtitle2">
You are following {itemName}
</HeaderText>
<BodyText variant="body2">
Unfollow to stop getting emails for new {itemName} courses.
</BodyText>
<Footer>
<Button
size="medium"
responsive={true}
variant="inverted"
edge="rounded"
data-testid="action-unfollow"
onClick={handleFollowAction}
>
Unfollow
</Button>
<Button
responsive={true}
size="medium"
edge="rounded"
onClick={() => props.onClose()}
>
Close
</Button>
</Footer>
</StyledPopover>
)
}
return (
<StyledPopover {...props} open={!!props.anchorEl}>
<HeaderText variant="subtitle2">Follow {itemName}?</HeaderText>
<BodyText variant="body2">
You will get an email when new courses are available.
</BodyText>
<Footer>
<Button
responsive={true}
size="medium"
edge="rounded"
variant="inverted"
onClick={() => props.onClose()}
>
Close
</Button>
<Button
responsive={true}
size="medium"
edge="rounded"
data-testid="action-follow"
onClick={handleFollowAction}
>
Follow
</Button>
</Footer>
</StyledPopover>
)
}

export { FollowPopover }
export type { FollowPopoverProps }
30 changes: 30 additions & 0 deletions frontends/mit-learn/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const {
CSRF_COOKIE_NAME,
APPZI_URL,
MITOL_NOINDEX,
DEFAULT_SEARCH_MODE,
DEFAULT_SEARCH_SLOP,
DEFAULT_SEARCH_STALENESS_PENALTY,
DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF,
DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY,
} = cleanEnv(process.env, {
NODE_ENV: str({
choices: ["development", "production", "test"],
Expand Down Expand Up @@ -124,6 +129,26 @@ const {
desc: "Whether to include a noindex meta tag",
default: true,
}),
DEFAULT_SEARCH_SLOP: num({
desc: "The default search slop",
default: 6,
}),
DEFAULT_SEARCH_STALENESS_PENALTY: num({
desc: "The default search staleness penalty",
default: 2.5,
}),
DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: num({
desc: "The default search minimum score cutoff",
default: 0,
}),
DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: num({
desc: "The default search max incompleteness penalty",
default: 90,
}),
DEFAULT_SEARCH_MODE: str({
desc: "The default search mode",
default: "phrase",
}),
})

const MITOL_FEATURES_PREFIX = "FEATURE_"
Expand Down Expand Up @@ -265,6 +290,11 @@ module.exports = (env, argv) => {
MITOL_SUPPORT_EMAIL: JSON.stringify(MITOL_SUPPORT_EMAIL),
PUBLIC_URL: JSON.stringify(PUBLIC_URL),
CSRF_COOKIE_NAME: JSON.stringify(CSRF_COOKIE_NAME),
DEFAULT_SEARCH_MODE: JSON.stringify(DEFAULT_SEARCH_MODE),
DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY,
DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF,
DEFAULT_SEARCH_SLOP,
DEFAULT_SEARCH_STALENESS_PENALTY,
},
}),
]
Expand Down
47 changes: 42 additions & 5 deletions learning_resources/etl/ocw.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ def parse_delivery(course_data: dict) -> list[str]:
return delivery


def parse_learn_topics(course_data: dict) -> list[dict]:
"""
Parse topics. Use the "mit_learn_topics" field if it exists and isn't empty,
otherwise use and transform the "topics" field values.

Args:
course_data (dict): The course data

Returns:
list[dict]: The topics
"""
mitlearn_topics = course_data.get("mit_learn_topics") or []
ocw_topics = course_data.get("topics") or []
if mitlearn_topics:
# Should already be in the correct format
return [
{"name": topic_name}
for topic_name in sorted(
{topic for topics in mitlearn_topics for topic in topics}
)
]
else:
# Topics need to be transformed
return transform_topics(
[
{"name": topic_name}
for topic_name in sorted(
{topic for topics in ocw_topics for topic in topics}
)
],
OFFERED_BY["code"],
)


def transform_content_files(
s3_resource: boto3.resource,
course_prefix: str,
Expand Down Expand Up @@ -329,10 +363,6 @@ def transform_course(course_data: dict) -> dict:
readable_term = f"+{slugify(term)}" if term else ""
readable_year = f"_{course_data.get('year')}" if year else ""
readable_id = f"{course_data[PRIMARY_COURSE_ID]}{readable_term}{readable_year}"
topics = transform_topics(
[{"name": topic} for topics in course_data.get("topics") for topic in topics],
OFFERED_BY["code"],
)
image_src = course_data.get("image_src")

return {
Expand Down Expand Up @@ -365,7 +395,14 @@ def transform_course(course_data: dict) -> dict:
is_ocw=True,
),
},
"topics": topics,
"topics": parse_learn_topics(course_data),
"ocw_topics": sorted(
{
topic_name
for topic_sublist in course_data.get("topics", [])
for topic_name in topic_sublist
}
),
"runs": [transform_run(course_data)],
"resource_type": LearningResourceType.course.name,
"unique_field": UNIQUE_FIELD,
Expand Down
Loading